java/com/facebook/soloader/SoLoader.java (897 lines of code) (raw):

/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.facebook.soloader; import android.annotation.TargetApi; import android.content.Context; import android.content.pm.ApplicationInfo; import android.os.Build; import android.os.StrictMode; import android.text.TextUtils; import android.util.Log; import com.facebook.soloader.nativeloader.NativeLoader; import dalvik.system.BaseDexClassLoader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.math.BigInteger; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantReadWriteLock; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.NotThreadSafe; import javax.annotation.concurrent.ThreadSafe; /** * Native code loader. * * <p>To load a native library, call the static method {@link #loadLibrary} from the static * initializer of the Java class declaring the native methods. The argument should be the library's * short name. * * <p>For example, if the native code is in libmy_jni_methods.so: * * <pre>{@code * class MyClass { * static { * SoLoader.loadLibrary("my_jni_methods"); * } * } * }</pre> * * <p>Before any library can be loaded SoLoader needs to be initialized. The application using * SoLoader should do that by calling SoLoader.init early on app initialization path. The call must * happen before any class using SoLoader in its static initializer is loaded. */ @ThreadSafe public class SoLoader { /* package */ static final String TAG = "SoLoader"; /* package */ static final boolean DEBUG = false; /* package */ static final boolean SYSTRACE_LIBRARY_LOADING; /* package */ @Nullable static SoFileLoader sSoFileLoader; /** * locking controlling the list of SoSources. We want to allow long running iterations over the * list to happen concurrently, but also ensure that nothing modifies the list while others are * reading it. */ private static final ReentrantReadWriteLock sSoSourcesLock = new ReentrantReadWriteLock(); /** * Ordered list of sources to consult when trying to load a shared library or one of its * dependencies. {@code null} indicates that SoLoader is uninitialized. */ @GuardedBy("sSoSourcesLock") @Nullable private static SoSource[] sSoSources = null; @GuardedBy("sSoSourcesLock") private static final AtomicInteger sSoSourcesVersion = new AtomicInteger(0); /** A backup SoSources to try if a lib file is corrupted */ @GuardedBy("sSoSourcesLock") @Nullable private static UnpackingSoSource[] sBackupSoSources; /** * A SoSource for the Context.ApplicationInfo.nativeLibsDir that can be updated if the application * moves this directory */ @GuardedBy("sSoSourcesLock") @Nullable private static ApplicationSoSource sApplicationSoSource; /** Records the sonames (e.g., "libdistract.so") of shared libraries we've loaded. */ @GuardedBy("SoLoader.class") private static final HashSet<String> sLoadedLibraries = new HashSet<>(); /** * Libraries that are in the process of being loaded, and lock objects to synchronize on and wait * for the loading to end. * * <p>To prevent potential deadlock, always acquire sSoSourcesLock before these locks! */ @GuardedBy("SoLoader.class") private static final Map<String, Object> sLoadingLibraries = new HashMap<>(); /** * Libraries that have loaded and completed their JniOnLoad. Reads and writes are guarded by the * corresponding lock in sLoadingLibraries. However, as we only add to this set (and never * remove), it is safe to perform un-guarded reads as an optional optimization prior to locking. */ private static final Set<String> sLoadedAndMergedLibraries = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>()); /** Wrapper for System.loadLibrary. */ @Nullable private static SystemLoadLibraryWrapper sSystemLoadLibraryWrapper = null; /** * Name of the directory we use for extracted DSOs from built-in SO sources (main APK, exopackage) */ private static final String SO_STORE_NAME_MAIN = "lib-main"; /** Name of the directory we use for extracted DSOs from split APKs */ private static final String SO_STORE_NAME_SPLIT = "lib-"; private static final String[] DEFAULT_DENY_LIST = new String[] {System.mapLibraryName("breakpad")}; /** Enable the exopackage SoSource. */ public static final int SOLOADER_ENABLE_EXOPACKAGE = (1 << 0); /** * Allow deferring some initialization work to asynchronous background threads. Shared libraries * are nevertheless ready to load as soon as init returns. */ public static final int SOLOADER_ALLOW_ASYNC_INIT = (1 << 1); public static final int SOLOADER_LOOK_IN_ZIP = (1 << 2); /** * In some contexts, using a backup so source in case of so corruption is not feasible e.g. lack * of write permissions to the library path. */ public static final int SOLOADER_DISABLE_BACKUP_SOSOURCE = (1 << 3); /** * Skip calling JNI_OnLoad if the library is merged. This is necessary for libraries that don't * define JNI_OnLoad and are only loaded for their side effects (like static constructors * registering callbacks). DO NOT use this to allow implicit JNI registration (by naming your * methods Java_com_facebook_whatever) because that is buggy on Android. */ public static final int SOLOADER_SKIP_MERGED_JNI_ONLOAD = (1 << 4); /** System Apps ignore PREFER_ANDROID_LIBS_DIRECTORY. Don't do that for this app. */ public static final int SOLOADER_DONT_TREAT_AS_SYSTEMAPP = (1 << 5); /** * In API level 23 and above, it’s possible to open a .so file directly from your APK. Enabling * this flag will explicitly add the direct SoSource in soSource list. */ public static final int SOLOADER_ENABLE_DIRECT_SOSOURCE = (1 << 6); /** * For compatibility, we need explicitly enable the backup soSource. This flag conflicts with * {@link #SOLOADER_DISABLE_BACKUP_SOSOURCE}, you should only set one of them or none. */ public static final int SOLOADER_EXPLICITLY_ENABLE_BACKUP_SOSOURCE = (1 << 7); @GuardedBy("sSoSourcesLock") private static int sFlags; interface AppType { public static final int UNSET = 0; /** Normal user installed APP */ public static final int THIRD_PARTY_APP = 1; /** The APP is installed in the device's system image. {@link ApplicationInfo#FLAG_SYSTEM} */ public static final int SYSTEM_APP = 2; /** * The APP has been installed as an update to a built-in system application. {@link * ApplicationInfo#FLAG_UPDATED_SYSTEM_APP} */ public static final int UPDATED_SYSTEM_APP = 3; } private static int sAppType = AppType.UNSET; static { boolean shouldSystrace = false; try { shouldSystrace = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2; } catch (NoClassDefFoundError | UnsatisfiedLinkError e) { // In some test contexts, the Build class and/or some of its dependencies do not exist. } SYSTRACE_LIBRARY_LOADING = shouldSystrace; } public static void init(Context context, int flags) throws IOException { init(context, flags, null, DEFAULT_DENY_LIST); } public static void init(Context context, int flags, @Nullable SoFileLoader soFileLoader) throws IOException { init(context, flags, soFileLoader, DEFAULT_DENY_LIST); } /** * Initializes native code loading for this app; this class's other static facilities cannot be * used until this {@link #init} is called. This method is idempotent: calls after the first are * ignored. * * @param context application context * @param flags Zero or more of the SOLOADER_* flags * @param soFileLoader the custom {@link SoFileLoader}, you can implement your own loader * @param denyList Skip load libs from system soSource, due to the linker namespace restriction */ public static void init( Context context, int flags, @Nullable SoFileLoader soFileLoader, String[] denyList) throws IOException { StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites(); try { sAppType = getAppType(context, flags); if ((flags & SOLOADER_EXPLICITLY_ENABLE_BACKUP_SOSOURCE) == 0 && SysUtil.isSupportedDirectLoad(context, sAppType)) { // SoLoader doesn't need backup soSource if it supports directly loading .so file from APK flags |= (SOLOADER_DISABLE_BACKUP_SOSOURCE | SOLOADER_ENABLE_DIRECT_SOSOURCE); } initSoLoader(soFileLoader); initSoSources(context, flags, denyList); NativeLoader.initIfUninitialized(new NativeLoaderToSoLoaderDelegate()); } finally { StrictMode.setThreadPolicy(oldPolicy); } } /** * Backward compatibility. * * @param context application context * @param nativeExopackage pass {@link #SOLOADER_ENABLE_EXOPACKAGE} as flags if true * @see <a href="https://buck.build/article/exopackage.html">What is exopackage?</a> */ public static void init(Context context, boolean nativeExopackage) { try { init(context, nativeExopackage ? SOLOADER_ENABLE_EXOPACKAGE : 0, null, DEFAULT_DENY_LIST); } catch (IOException ex) { throw new RuntimeException(ex); } } private static void initSoSources(Context context, int flags, String[] denyList) throws IOException { if (sSoSources != null) { return; } sSoSourcesLock.writeLock().lock(); try { sFlags = flags; ArrayList<SoSource> soSources = new ArrayList<>(); AddSystemLibSoSource(soSources, denyList); // // We can only proceed forward if we have a Context. The prominent case // where we don't have a Context is barebones dalvikvm instantiations. In // that case, the caller is responsible for providing a correct LD_LIBRARY_PATH. // if (context != null) { // // Prepend our own SoSource for our own DSOs. // if ((flags & SOLOADER_ENABLE_EXOPACKAGE) != 0) { sBackupSoSources = null; if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "adding exo package source: " + SO_STORE_NAME_MAIN); } soSources.add(0, new ExoSoSource(context, SO_STORE_NAME_MAIN)); } else { if ((flags & SOLOADER_ENABLE_DIRECT_SOSOURCE) != 0) { addDirectApkSoSource(context, soSources); } addApplicationSoSource(context, soSources, getApplicationSoSourceFlags()); AddBackupSoSource(context, soSources, ApkSoSource.PREFER_ANDROID_LIBS_DIRECTORY); } } SoSource[] finalSoSources = soSources.toArray(new SoSource[soSources.size()]); int prepareFlags = makePrepareFlags(); for (int i = finalSoSources.length; i-- > 0; ) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Preparing SO source: " + finalSoSources[i]); } finalSoSources[i].prepare(prepareFlags); } sSoSources = finalSoSources; sSoSourcesVersion.getAndIncrement(); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "init finish: " + sSoSources.length + " SO sources prepared"); } } finally { sSoSourcesLock.writeLock().unlock(); } } private static int getApplicationSoSourceFlags() { switch (sAppType) { case AppType.THIRD_PARTY_APP: return 0; case AppType.SYSTEM_APP: case AppType.UPDATED_SYSTEM_APP: return DirectorySoSource.RESOLVE_DEPENDENCIES; default: throw new RuntimeException("Unsupported app type, we should not reach here"); } } /** Add DirectApk SoSources for disabling android:extractNativeLibs */ private static void addDirectApkSoSource(Context context, ArrayList<SoSource> soSources) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && context.getApplicationInfo().splitSourceDirs != null) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "adding directApk sources from split apks"); } for (String splitApkDir : context.getApplicationInfo().splitSourceDirs) { DirectApkSoSource splitApkSource = new DirectApkSoSource(new File(splitApkDir)); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "adding directApk source: " + splitApkSource.toString()); } soSources.add(0, splitApkSource); } } DirectApkSoSource directApkSoSource = new DirectApkSoSource(context); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "adding directApk source: " + directApkSoSource.toString()); } soSources.add(0, directApkSoSource); } /** Add a DirectorySoSource for the application's nativeLibraryDir . */ private static void addApplicationSoSource( Context context, ArrayList<SoSource> soSources, int flags) { // On old versions of Android, Bionic doesn't add our library directory to its // internal search path, and the system doesn't resolve dependencies between // modules we ship. On these systems, we resolve dependencies ourselves. On other // systems, Bionic's built-in resolver suffices. if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR1) { flags |= DirectorySoSource.RESOLVE_DEPENDENCIES; } sApplicationSoSource = new ApplicationSoSource(context, flags); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "adding application source: " + sApplicationSoSource.toString()); } soSources.add(0, sApplicationSoSource); } /** Add the SoSources for recovering the dso if the file is corrupted or missed */ private static void AddBackupSoSource( Context context, ArrayList<SoSource> soSources, int apkSoSourceFlags) throws IOException { if ((sFlags & SOLOADER_DISABLE_BACKUP_SOSOURCE) != 0) { sBackupSoSources = null; // Clean up backups final File backupDir = UnpackingSoSource.getSoStorePath(context, SO_STORE_NAME_MAIN); try { SysUtil.dumbDeleteRecursive(backupDir); } catch (IOException e) { Log.w(TAG, "Failed to delete " + backupDir.getCanonicalPath(), e); } return; } final File mainApkDir = new File(context.getApplicationInfo().sourceDir); ArrayList<UnpackingSoSource> backupSources = new ArrayList<>(); ApkSoSource mainApkSource = new ApkSoSource(context, mainApkDir, SO_STORE_NAME_MAIN, apkSoSourceFlags); backupSources.add(mainApkSource); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "adding backup source from : " + mainApkSource.toString()); } addBackupSoSourceFromSplitApk(context, apkSoSourceFlags, backupSources); sBackupSoSources = backupSources.toArray(new UnpackingSoSource[backupSources.size()]); soSources.addAll(0, backupSources); } private static void addBackupSoSourceFromSplitApk( Context context, int apkSoSourceFlags, ArrayList<UnpackingSoSource> backupSources) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && context.getApplicationInfo().splitSourceDirs != null) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "adding backup sources from split apks"); } int splitIndex = 0; for (String splitApkDir : context.getApplicationInfo().splitSourceDirs) { ApkSoSource splitApkSource = new ApkSoSource( context, new File(splitApkDir), SO_STORE_NAME_SPLIT + (splitIndex++), apkSoSourceFlags); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "adding backup source: " + splitApkSource.toString()); } backupSources.add(splitApkSource); } } } /** * Add SoSource objects for each of the system library directories. * * @param soSources target soSource list * @param denyList Skip load libs from current soSource, due to the linker namespace restriction */ private static void AddSystemLibSoSource(ArrayList<SoSource> soSources, String[] denyList) { String systemLibPaths = SysUtil.is64Bit() ? "/system/lib64:/vendor/lib64" : "/system/lib:/vendor/lib"; String LD_LIBRARY_PATH = System.getenv("LD_LIBRARY_PATH"); if (LD_LIBRARY_PATH != null && !LD_LIBRARY_PATH.equals("")) { systemLibPaths += ":" + LD_LIBRARY_PATH; } final Set<String> libPathSet = new HashSet<>(Arrays.asList(systemLibPaths.split(":"))); for (String libPath : libPathSet) { // Don't pass DirectorySoSource.RESOLVE_DEPENDENCIES for directories we find on // LD_LIBRARY_PATH: Bionic's dynamic linker is capable of correctly resolving dependencies // these libraries have on each other, so doing that ourselves would be a waste. if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "adding system library source: " + libPath); } File systemSoDirectory = new File(libPath); soSources.add( new DirectorySoSource(systemSoDirectory, DirectorySoSource.ON_LD_LIBRARY_PATH, denyList)); } } private static int makePrepareFlags() { int prepareFlags = 0; // ensure the write lock is being held to protect sFlags // this is used when preparing new SoSources in the list. sSoSourcesLock.writeLock().lock(); try { if ((sFlags & SOLOADER_ALLOW_ASYNC_INIT) != 0) { prepareFlags |= SoSource.PREPARE_FLAG_ALLOW_ASYNC_INIT; } return prepareFlags; } finally { sSoSourcesLock.writeLock().unlock(); } } private static synchronized void initSoLoader(@Nullable SoFileLoader soFileLoader) { if (soFileLoader == null && sSoFileLoader != null) { return; } if (soFileLoader != null) { sSoFileLoader = soFileLoader; return; } final Runtime runtime = Runtime.getRuntime(); final Method nativeLoadRuntimeMethod = getNativeLoadRuntimeMethod(); final boolean hasNativeLoadMethod = nativeLoadRuntimeMethod != null; final String localLdLibraryPath = hasNativeLoadMethod ? Api14Utils.getClassLoaderLdLoadLibrary() : null; final String localLdLibraryPathNoZips = makeNonZipPath(localLdLibraryPath); sSoFileLoader = new SoFileLoader() { @Override public void loadBytes(String pathName, ElfByteChannel bytes, int loadFlags) { throw new UnsupportedOperationException(); } @Override public void load(final String pathToSoFile, final int loadFlags) { String error = null; if (hasNativeLoadMethod) { final boolean inZip = (loadFlags & SOLOADER_LOOK_IN_ZIP) == SOLOADER_LOOK_IN_ZIP; final String path = inZip ? localLdLibraryPath : localLdLibraryPathNoZips; try { synchronized (runtime) { error = (String) nativeLoadRuntimeMethod.invoke( runtime, pathToSoFile, SoLoader.class.getClassLoader(), path); if (error != null) { throw new UnsatisfiedLinkError(error); } } } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { error = "Error: Cannot load " + pathToSoFile; throw new RuntimeException(error, e); } finally { if (error != null) { Log.e( TAG, "Error when loading lib: " + error + " lib hash: " + getLibHash(pathToSoFile) + " search path is " + path); } } } else { System.load(pathToSoFile); } } /** * Logs MD5 of lib that failed loading */ private String getLibHash(String libPath) { String digestStr; try { File libFile = new File(libPath); MessageDigest digest = MessageDigest.getInstance("MD5"); try (InputStream libInStream = new FileInputStream(libFile)) { byte[] buffer = new byte[4096]; int bytesRead; while ((bytesRead = libInStream.read(buffer)) > 0) { digest.update(buffer, 0, bytesRead); } digestStr = String.format("%32x", new BigInteger(1, digest.digest())); } } catch (IOException | SecurityException | NoSuchAlgorithmException e) { digestStr = e.toString(); } return digestStr; } }; } private static @Nullable Method getNativeLoadRuntimeMethod() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || Build.VERSION.SDK_INT > 27) { return null; } try { final Method method = Runtime.class.getDeclaredMethod( "nativeLoad", String.class, ClassLoader.class, String.class); method.setAccessible(true); return method; } catch (final NoSuchMethodException | SecurityException e) { Log.w(TAG, "Cannot get nativeLoad method", e); return null; } } private static int getAppType(Context context, int flags) { if (sAppType != AppType.UNSET) { return sAppType; } if ((flags & SOLOADER_DONT_TREAT_AS_SYSTEMAPP) != 0 || context == null) { return AppType.THIRD_PARTY_APP; } final int type; final ApplicationInfo appInfo = context.getApplicationInfo(); if ((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) { type = AppType.THIRD_PARTY_APP; } else if ((appInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0) { type = AppType.UPDATED_SYSTEM_APP; } else { type = AppType.SYSTEM_APP; } if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "ApplicationInfo.flags is: " + appInfo.flags + " appType is: " + type); } return type; } /** Turn shared-library loading into a no-op. Useful in special circumstances. */ public static void setInTestMode() { TestOnlyUtils.setSoSources(new SoSource[] {new NoopSoSource()}); } /** Make shared-library loading delegate to the system. Useful for tests. */ public static void deinitForTest() { TestOnlyUtils.setSoSources(null); } @NotThreadSafe static class TestOnlyUtils { /* Set so sources. Useful for tests. */ /* package */ static void setSoSources(SoSource[] sources) { sSoSourcesLock.writeLock().lock(); try { sSoSources = sources; sSoSourcesVersion.getAndIncrement(); } finally { sSoSourcesLock.writeLock().unlock(); } } /** * Set so file loader. <br> * N.B. <b>ONLY FOR TESTS</b>. It has read/write race with {@code * SoLoader.sSoFileLoader.load(String, int)} in {@link DirectorySoSource#loadLibraryFrom} * * @param loader {@link SoFileLoader} */ /* package */ static void setSoFileLoader(SoFileLoader loader) { sSoFileLoader = loader; } /** Reset internal status. Only for tests. */ /* package */ static void resetStatus() { synchronized (SoLoader.class) { sLoadedLibraries.clear(); sLoadingLibraries.clear(); sSoFileLoader = null; } setSoSources(null); } } /** * Provide a wrapper object for calling {@link System#loadLibrary}. This is useful for controlling * which ClassLoader libraries are loaded into. */ public static void setSystemLoadLibraryWrapper(SystemLoadLibraryWrapper wrapper) { sSystemLoadLibraryWrapper = wrapper; } public static final class WrongAbiError extends UnsatisfiedLinkError { WrongAbiError(Throwable cause, String machine) { super( "APK was built for a different platform. Supported ABIs: " + Arrays.toString(SysUtil.getSupportedAbis()) + " error: " + machine); initCause(cause); } } /** * Gets the full path of a library. * * @param libName the library file name, including the prefix and extension. * @return the full path of the library, or null if it is not found in none of the SoSources. * @throws IOException if there is an error calculating the canonical path of libName */ public static @Nullable String getLibraryPath(String libName) throws IOException { String libPath = null; sSoSourcesLock.readLock().lock(); try { if (sSoSources != null) { for (int i = 0; libPath == null && i < sSoSources.length; ++i) { SoSource currentSource = sSoSources[i]; libPath = currentSource.getLibraryPath(libName); } } } finally { sSoSourcesLock.readLock().unlock(); } return libPath; } /** * Gets the dependencies of a library. * * @param libName the library file name, including the prefix and extension. * @return An array naming the dependencies of the library, or null if it is not found in any * SoSources * @throws IOException if there is an error reading libName */ public static @Nullable String[] getLibraryDependencies(String libName) throws IOException { String[] deps = null; sSoSourcesLock.readLock().lock(); try { if (sSoSources != null) { for (int i = 0; deps == null && i < sSoSources.length; ++i) { SoSource currentSource = sSoSources[i]; deps = currentSource.getLibraryDependencies(libName); } } } finally { sSoSourcesLock.readLock().unlock(); } return deps; } /** * Returns the so file for the specified library. Returns null if the library does not exist or if * it's not backed by a file. */ public static @Nullable File getSoFile(String shortName) { String mergedLibName = MergedSoMapping.mapLibName(shortName); String soName = mergedLibName != null ? mergedLibName : shortName; String mappedName = System.mapLibraryName(soName); sSoSourcesLock.readLock().lock(); try { if (sSoSources != null) { for (int i = 0; i < sSoSources.length; ++i) { SoSource currentSource = sSoSources[i]; try { File soFile = currentSource.getSoFileByName(mappedName); if (soFile != null) { return soFile; } } catch (IOException e) { // Failed to get the file, let's skip this so source. } } } } finally { sSoSourcesLock.readLock().unlock(); } return null; } public static boolean loadLibrary(String shortName) { return loadLibrary(shortName, 0); } /** * Load a shared library, initializing any JNI binding it contains. * * @param shortName Name of library to find, without "lib" prefix or ".so" suffix * @param loadFlags Control flags for the loading behavior. See available flags under {@link * SoSource} (LOAD_FLAG_XXX). * @return Whether the library was loaded as a result of this call (true), or was already loaded * through a previous call to SoLoader (false). */ public static boolean loadLibrary(String shortName, int loadFlags) throws UnsatisfiedLinkError { Boolean needsLoad = loadLibraryOnNonAndroid(shortName); if (needsLoad != null) { return needsLoad; } // This is to account for the fact that we want to load .so files from the apk itself when it is // a system app. if ((sAppType == AppType.SYSTEM_APP || sAppType == AppType.UPDATED_SYSTEM_APP) && sSystemLoadLibraryWrapper != null) { sSystemLoadLibraryWrapper.loadLibrary(shortName); return true; } String mergedLibName = MergedSoMapping.mapLibName(shortName); String soName = mergedLibName != null ? mergedLibName : shortName; return loadLibraryBySoName( System.mapLibraryName(soName), shortName, mergedLibName, loadFlags, null); } private static @Nullable Boolean loadLibraryOnNonAndroid(String shortName) { if (sSoSources == null) { sSoSourcesLock.readLock().lock(); try { if (sSoSources == null) { // This should never happen during normal operation, // but if we're running in a non-Android environment, // fall back to System.loadLibrary. if ("http://www.android.com/".equals(System.getProperty("java.vendor.url"))) { // This will throw. assertInitialized(); } else { // Not on an Android system. Ask the JVM to load for us. synchronized (SoLoader.class) { boolean needsLoad = !sLoadedLibraries.contains(shortName); if (needsLoad) { if (sSystemLoadLibraryWrapper != null) { sSystemLoadLibraryWrapper.loadLibrary(shortName); } else { System.loadLibrary(shortName); } } return needsLoad; } } } } finally { sSoSourcesLock.readLock().unlock(); } } return null; } /* package */ static void loadLibraryBySoName( String soName, int loadFlags, StrictMode.ThreadPolicy oldPolicy) { loadLibraryBySoNameImpl(soName, null, null, loadFlags, oldPolicy); } private static boolean loadLibraryBySoName( String soName, @Nullable String shortName, @Nullable String mergedLibName, int loadFlags, @Nullable StrictMode.ThreadPolicy oldPolicy) { boolean ret = false; boolean retry; do { retry = false; try { ret = loadLibraryBySoNameImpl(soName, shortName, mergedLibName, loadFlags, oldPolicy); } catch (UnsatisfiedLinkError e) { final int currentVersion = sSoSourcesVersion.get(); sSoSourcesLock.writeLock().lock(); try { if (sApplicationSoSource != null && sApplicationSoSource.checkAndMaybeUpdate()) { Log.w( TAG, "sApplicationSoSource updated during load: " + soName + ", attempting load again."); sSoSourcesVersion.getAndIncrement(); retry = true; } } catch (IOException ex) { throw new RuntimeException(ex); } finally { sSoSourcesLock.writeLock().unlock(); } if (sSoSourcesVersion.get() == currentVersion) { // nothing changed in soSource, Propagate original error throw e; } } } while (retry); return ret; } private static boolean loadLibraryBySoNameImpl( String soName, @Nullable String shortName, @Nullable String mergedLibName, int loadFlags, @Nullable StrictMode.ThreadPolicy oldPolicy) { // As an optimization, avoid taking locks if the library has already loaded. Without locks this // does not provide 100% coverage (e.g. another thread may currently be loading or initializing // the library), so we'll need to check again with locks held, below. if (!TextUtils.isEmpty(shortName) && sLoadedAndMergedLibraries.contains(shortName)) { return false; } // LoadingLibLock is used to ensure that doLoadLibraryBySoName and its corresponding JniOnload // are only executed once per library. It also guarantees that concurrent calls to loadLibrary // for the same library do not return until both its load and JniOnLoad have completed. Object loadingLibLock; boolean loaded = false; synchronized (SoLoader.class) { if (sLoadedLibraries.contains(soName)) { if (mergedLibName == null) { // Not a merged lib, no need to init return false; } loaded = true; } if (sLoadingLibraries.containsKey(soName)) { loadingLibLock = sLoadingLibraries.get(soName); } else { loadingLibLock = new Object(); sLoadingLibraries.put(soName, loadingLibLock); } } // Note that both doLoadLibraryBySoName and invokeJniOnload (below) may re-enter loadLibrary // (or loadLibraryBySoNameImpl), recursively acquiring additional library and soSource locks. // // To avoid bi-directional lock usage (threadA takes loadingLibLock then sSoSourcesLock, threadB // takes sSoSourcesLock then loadingLibLock) and potential deadlock as this method recursively // calls loadLibrary, we must acquire sSoSourcesLock first. sSoSourcesLock.readLock().lock(); try { synchronized (loadingLibLock) { if (!loaded) { synchronized (SoLoader.class) { if (sLoadedLibraries.contains(soName)) { // Library was successfully loaded by other thread while we waited if (mergedLibName == null) { // Not a merged lib, no need to init return false; } loaded = true; } // Else, load was not successful on other thread. We will try in this one. } if (!loaded) { try { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "About to load: " + soName); } doLoadLibraryBySoName(soName, loadFlags, oldPolicy); } catch (UnsatisfiedLinkError ex) { String message = ex.getMessage(); if (message != null && message.contains("unexpected e_machine:")) { String machine_msg = message.substring(message.lastIndexOf("unexpected e_machine:")); throw new WrongAbiError(ex, machine_msg); } throw ex; } if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Loaded: " + soName); } synchronized (SoLoader.class) { sLoadedLibraries.add(soName); } } } if ((loadFlags & SOLOADER_SKIP_MERGED_JNI_ONLOAD) == 0) { // MergedSoMapping#invokeJniOnload does not necessarily handle concurrent nor redundant // invocation. sLoadedAndMergedLibraries is used in conjunction with loadingLibLock to // ensure one invocation per library. boolean isAlreadyMerged = !TextUtils.isEmpty(shortName) && sLoadedAndMergedLibraries.contains(shortName); if (mergedLibName != null && !isAlreadyMerged) { if (SYSTRACE_LIBRARY_LOADING) { Api18TraceUtils.beginTraceSection("MergedSoMapping.invokeJniOnload[", shortName, "]"); } try { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "About to merge: " + shortName + " / " + soName); } MergedSoMapping.invokeJniOnload(shortName); sLoadedAndMergedLibraries.add(shortName); } catch (UnsatisfiedLinkError e) { // If you are seeing this exception, first make sure your library sets // allow_jni_merging=True. Trying to merge a library without that // will trigger this error. If that's already in place, you're probably // not defining JNI_OnLoad. Calling SoLoader.loadLibrary on a library // that doesn't define JNI_OnLoad is a no-op when that library is not merged. // Once you enable merging, it throws an UnsatisfiedLinkError. // There are three main reasons a library might not define JNI_OnLoad, // and the solution depends on which case you have. // - You might be using implicit registration (native methods defined like // `Java_com_facebook_Foo_bar(JNIEnv* env)`). This is not safe on Android // https://fb.workplace.com/groups/442333009148653/permalink/651212928260659/ // and is not compatible with FBJNI. Stop doing it. Use FBJNI registerNatives. // - You might have a C++-only library with no JNI bindings and no static // initializers with side-effects. You can just delete the loadLibrary call. // - You might have a C++-only library that needs to be loaded explicitly because // it has static initializers whose side-effects are needed. In that case, // pass the SOLOADER_SKIP_MERGED_JNI_ONLOAD flag to loadLibrary. throw new RuntimeException( "Failed to call JNI_OnLoad from '" + shortName + "', which has been merged into '" + soName + "'. See comment for details.", e); } finally { if (SYSTRACE_LIBRARY_LOADING) { Api18TraceUtils.endSection(); } } } } } } finally { sSoSourcesLock.readLock().unlock(); } return !loaded; } /** * Unpack library and its dependencies, returning the location of the unpacked library file. All * non-system dependencies of the given library will either be on LD_LIBRARY_PATH or will be in * the same directory as the returned File. * * @param shortName Name of library to find, without "lib" prefix or ".so" suffix * @return Unpacked DSO location */ public static File unpackLibraryAndDependencies(String shortName) throws UnsatisfiedLinkError { assertInitialized(); try { return unpackLibraryBySoName(System.mapLibraryName(shortName)); } catch (IOException ex) { throw new RuntimeException(ex); } } private static void doLoadLibraryBySoName( String soName, int loadFlags, @Nullable StrictMode.ThreadPolicy oldPolicy) throws UnsatisfiedLinkError { int result = SoSource.LOAD_RESULT_NOT_FOUND; sSoSourcesLock.readLock().lock(); try { if (sSoSources == null) { Log.e(TAG, "Could not load: " + soName + " because no SO source exists"); throw new UnsatisfiedLinkError("couldn't find DSO to load: " + soName); } } finally { sSoSourcesLock.readLock().unlock(); } // This way, we set the thread policy only one per loadLibrary no matter how many // dependencies we load. Each call to StrictMode.allowThreadDiskWrites allocates. boolean restoreOldPolicy = false; if (oldPolicy == null) { oldPolicy = StrictMode.allowThreadDiskReads(); restoreOldPolicy = true; } if (SYSTRACE_LIBRARY_LOADING) { Api18TraceUtils.beginTraceSection("SoLoader.loadLibrary[", soName, "]"); } Throwable error = null; try { sSoSourcesLock.readLock().lock(); try { for (int i = 0; result == SoSource.LOAD_RESULT_NOT_FOUND && i < sSoSources.length; ++i) { SoSource currentSource = sSoSources[i]; result = currentSource.loadLibrary(soName, loadFlags, oldPolicy); if (result == SoSource.LOAD_RESULT_CORRUPTED_LIB_FILE && sBackupSoSources != null) { // Let's try from the backup source if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Trying backup SoSource for " + soName); } for (UnpackingSoSource backupSoSource : sBackupSoSources) { backupSoSource.prepare(soName); int resultFromBackup = backupSoSource.loadLibrary(soName, loadFlags, oldPolicy); if (resultFromBackup == SoSource.LOAD_RESULT_LOADED) { result = resultFromBackup; break; } } break; } } } finally { sSoSourcesLock.readLock().unlock(); } } catch (Throwable t) { error = t; } finally { if (SYSTRACE_LIBRARY_LOADING) { Api18TraceUtils.endSection(); } if (restoreOldPolicy) { StrictMode.setThreadPolicy(oldPolicy); } if (result == SoSource.LOAD_RESULT_NOT_FOUND || result == SoSource.LOAD_RESULT_CORRUPTED_LIB_FILE) { StringBuilder sb = new StringBuilder().append("couldn't find DSO to load: ").append(soName); if (error != null) { String cause = error.getMessage(); if (cause == null) { cause = error.toString(); } sb.append(" caused by: ").append(cause); error.printStackTrace(); } else { // load failure wasn't caused by dependent libraries. // Print the sources and current native library directory sSoSourcesLock.readLock().lock(); for (int i = 0; i < sSoSources.length; ++i) { sb.append("\n\tSoSource ").append(i).append(": ").append(sSoSources[i].toString()); } if (sApplicationSoSource != null) { Context updatedContext = sApplicationSoSource.getUpdatedContext(); File updatedNativeLibDir = ApplicationSoSource.getNativeLibDirFromContext(updatedContext); sb.append("\n\tNative lib dir: ") .append(updatedNativeLibDir.getAbsolutePath()) .append("\n"); } sSoSourcesLock.readLock().unlock(); } sb.append(" result: ").append(result); final String message = sb.toString(); Log.e(TAG, message); UnsatisfiedLinkError err = new UnsatisfiedLinkError(message); if (error != null) { err.initCause(error); } throw err; } } } @Nullable public static String makeNonZipPath(final String localLdLibraryPath) { if (localLdLibraryPath == null) { return null; } final String[] paths = localLdLibraryPath.split(":"); final ArrayList<String> pathsWithoutZip = new ArrayList<String>(paths.length); for (final String path : paths) { if (path.contains("!")) { continue; } pathsWithoutZip.add(path); } return TextUtils.join(":", pathsWithoutZip); } /* package */ static File unpackLibraryBySoName(String soName) throws IOException { sSoSourcesLock.readLock().lock(); try { for (SoSource soSource : sSoSources) { File unpacked = soSource.unpackLibrary(soName); if (unpacked != null) { return unpacked; } } } finally { sSoSourcesLock.readLock().unlock(); } throw new FileNotFoundException(soName); } private static void assertInitialized() { if (!isInitialized()) { throw new IllegalStateException("SoLoader.init() not yet called"); } } public static boolean isInitialized() { sSoSourcesLock.readLock().lock(); try { return sSoSources != null; } finally { sSoSourcesLock.readLock().unlock(); } } public static int getSoSourcesVersion() { return sSoSourcesVersion.get(); } /** * Add a new source of native libraries. SoLoader consults the new source before any * currently-installed source. * * @param extraSoSource The SoSource to install */ public static void prependSoSource(SoSource extraSoSource) throws IOException { sSoSourcesLock.writeLock().lock(); try { assertInitialized(); extraSoSource.prepare(makePrepareFlags()); SoSource[] newSoSources = new SoSource[sSoSources.length + 1]; newSoSources[0] = extraSoSource; System.arraycopy(sSoSources, 0, newSoSources, 1, sSoSources.length); sSoSources = newSoSources; sSoSourcesVersion.getAndIncrement(); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Prepended to SO sources: " + extraSoSource); } } finally { sSoSourcesLock.writeLock().unlock(); } } /** * Retrieve an LD_LIBRARY_PATH value suitable for using the native linker to resolve our shared * libraries. */ public static String makeLdLibraryPath() { sSoSourcesLock.readLock().lock(); try { assertInitialized(); ArrayList<String> pathElements = new ArrayList<>(); SoSource[] soSources = sSoSources; if (soSources != null) { for (SoSource soSource : soSources) { soSource.addToLdLibraryPath(pathElements); } } String joinedPaths = TextUtils.join(":", pathElements); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "makeLdLibraryPath final path: " + joinedPaths); } return joinedPaths; } finally { sSoSourcesLock.readLock().unlock(); } } /** * This function ensure that every SoSources Abi is supported for at least one abi in * SysUtil.getSupportedAbis * * @return true if all SoSources have their Abis supported */ public static boolean areSoSourcesAbisSupported() { sSoSourcesLock.readLock().lock(); try { if (sSoSources == null) { return false; } String[] supportedAbis = SysUtil.getSupportedAbis(); for (SoSource soSource : sSoSources) { String[] soSourceAbis = soSource.getSoSourceAbis(); for (String soSourceAbi : soSourceAbis) { boolean soSourceSupported = false; for (int k = 0; k < supportedAbis.length && !soSourceSupported; ++k) { soSourceSupported = soSourceAbi.equals(supportedAbis[k]); } if (!soSourceSupported) { Log.e(TAG, "abi not supported: " + soSourceAbi); return false; } } } return true; } finally { sSoSourcesLock.readLock().unlock(); } } public static boolean useDepsFile(Context context, String depsFilePath) throws IOException { File apkFile = new File(context.getApplicationInfo().sourceDir); byte[] apkId = SysUtil.makeApkDepBlock(apkFile, context); return NativeDeps.useDepsFile(apkId, depsFilePath); } @DoNotOptimize @TargetApi(14) /* package */ static class Api14Utils { public static String getClassLoaderLdLoadLibrary() { final ClassLoader classLoader = SoLoader.class.getClassLoader(); if (classLoader != null && !(classLoader instanceof BaseDexClassLoader)) { throw new IllegalStateException( "ClassLoader " + classLoader.getClass().getName() + " should be of type BaseDexClassLoader"); } try { final BaseDexClassLoader baseDexClassLoader = (BaseDexClassLoader) classLoader; final Method getLdLibraryPathMethod = BaseDexClassLoader.class.getMethod("getLdLibraryPath"); return (String) getLdLibraryPathMethod.invoke(baseDexClassLoader); } catch (Exception e) { throw new RuntimeException("Cannot call getLdLibraryPath", e); } } } }