/* * Copyright (C) 2011 The Android Open Source Project * * 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 dalvik.system; import android.compat.annotation.UnsupportedAppUsage; import android.system.ErrnoException; import android.system.StructStat; import java.io.File; import java.io.IOException; import java.lang.reflect.Array; import java.net.MalformedURLException; import java.net.URL; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.List; import java.util.Objects; import libcore.io.ClassPathURLStreamHandler; import libcore.io.IoUtils; import libcore.io.Libcore; import static android.system.OsConstants.S_ISDIR; /** * A pair of lists of entries, associated with a {@code ClassLoader}. * One of the lists is a dex/resource path — typically referred * to as a "class path" — list, and the other names directories * containing native code libraries. Class path entries may be any of: * a {@code .jar} or {@code .zip} file containing an optional * top-level {@code classes.dex} file as well as arbitrary resources, * or a plain {@code .dex} file (with no possibility of associated * resources). * *

This class also contains methods to use these lists to look up * classes and resources.

* * @hide */ public final class DexPathList { private static final String DEX_SUFFIX = ".dex"; private static final String zipSeparator = "!/"; /** class definition context */ @UnsupportedAppUsage private final ClassLoader definingContext; /** * List of dex/resource (class path) elements. * Should be called pathElements, but the Facebook app uses reflection * to modify 'dexElements' (http://b/7726934). */ @UnsupportedAppUsage private Element[] dexElements; /** List of native library path elements. */ // Some applications rely on this field being an array or we'd use a final list here @UnsupportedAppUsage /* package visible for testing */ NativeLibraryElement[] nativeLibraryPathElements; /** List of application native library directories. */ @UnsupportedAppUsage private final List nativeLibraryDirectories; /** List of system native library directories. */ @UnsupportedAppUsage private final List systemNativeLibraryDirectories; /** * Exceptions thrown during creation of the dexElements list. */ @UnsupportedAppUsage private IOException[] dexElementsSuppressedExceptions; private List getAllNativeLibraryDirectories() { List allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories); allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories); return allNativeLibraryDirectories; } /** * Construct an instance. * * @param definingContext the context in which any as-yet unresolved * classes should be defined * * @param dexFiles the bytebuffers containing the dex files that we should load classes from. */ public DexPathList(ClassLoader definingContext, String librarySearchPath) { if (definingContext == null) { throw new NullPointerException("definingContext == null"); } this.definingContext = definingContext; this.nativeLibraryDirectories = splitPaths(librarySearchPath, false); this.systemNativeLibraryDirectories = splitPaths(System.getProperty("java.library.path"), true); this.nativeLibraryPathElements = makePathElements(getAllNativeLibraryDirectories()); } /** * Constructs an instance. * * @param definingContext the context in which any as-yet unresolved * classes should be defined * @param dexPath list of dex/resource path elements, separated by * {@code File.pathSeparator} * @param librarySearchPath list of native library directory path elements, * separated by {@code File.pathSeparator} * @param optimizedDirectory directory where optimized {@code .dex} files * should be found and written to, or {@code null} to use the default * system directory for same */ @UnsupportedAppUsage public DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory) { this(definingContext, dexPath, librarySearchPath, optimizedDirectory, false); } DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory, boolean isTrusted) { if (definingContext == null) { throw new NullPointerException("definingContext == null"); } if (dexPath == null) { throw new NullPointerException("dexPath == null"); } if (optimizedDirectory != null) { if (!optimizedDirectory.exists()) { throw new IllegalArgumentException( "optimizedDirectory doesn't exist: " + optimizedDirectory); } if (!(optimizedDirectory.canRead() && optimizedDirectory.canWrite())) { throw new IllegalArgumentException( "optimizedDirectory not readable/writable: " + optimizedDirectory); } } this.definingContext = definingContext; ArrayList suppressedExceptions = new ArrayList(); // save dexPath for BaseDexClassLoader this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions, definingContext, isTrusted); // Native libraries may exist in both the system and // application library paths, and we use this search order: // // 1. This class loader's library path for application libraries (librarySearchPath): // 1.1. Native library directories // 1.2. Path to libraries in apk-files // 2. The VM's library path from the system property for system libraries // also known as java.library.path // // This order was reversed prior to Gingerbread; see http://b/2933456. this.nativeLibraryDirectories = splitPaths(librarySearchPath, false); this.systemNativeLibraryDirectories = splitPaths(System.getProperty("java.library.path"), true); this.nativeLibraryPathElements = makePathElements(getAllNativeLibraryDirectories()); if (suppressedExceptions.size() > 0) { this.dexElementsSuppressedExceptions = suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]); } else { dexElementsSuppressedExceptions = null; } } @Override public String toString() { return "DexPathList[" + Arrays.toString(dexElements) + ",nativeLibraryDirectories=" + Arrays.toString(getAllNativeLibraryDirectories().toArray()) + "]"; } /** * For BaseDexClassLoader.getLdLibraryPath. */ public List getNativeLibraryDirectories() { return nativeLibraryDirectories; } /** * Adds a new path to this instance * @param dexPath list of dex/resource path element, separated by * {@code File.pathSeparator} * @param optimizedDirectory directory where optimized {@code .dex} files * should be found and written to, or {@code null} to use the default * system directory for same */ @UnsupportedAppUsage public void addDexPath(String dexPath, File optimizedDirectory) { addDexPath(dexPath, optimizedDirectory, false); } public void addDexPath(String dexPath, File optimizedDirectory, boolean isTrusted) { final List suppressedExceptionList = new ArrayList(); final Element[] newElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptionList, definingContext, isTrusted); if (newElements != null && newElements.length > 0) { dexElements = concat(Element.class, dexElements, newElements); } if (suppressedExceptionList.size() > 0) { final IOException[] newSuppExceptions = suppressedExceptionList.toArray( new IOException[suppressedExceptionList.size()]); dexElementsSuppressedExceptions = dexElementsSuppressedExceptions != null ? concat(IOException.class, dexElementsSuppressedExceptions, newSuppExceptions) : newSuppExceptions; } } private static T[] concat(Class componentType, T[] inputA, T[] inputB) { T[] output = (T[]) Array.newInstance(componentType, inputA.length + inputB.length); System.arraycopy(inputA, 0, output, 0, inputA.length); System.arraycopy(inputB, 0, output, inputA.length, inputB.length); return output; } /** * For InMemoryDexClassLoader. Initializes {@code dexElements} with dex files * loaded from {@code dexFiles} buffers. * * @param dexFiles ByteBuffers containing raw dex data. Apks are not supported. */ /* package */ void initByteBufferDexPath(ByteBuffer[] dexFiles) { if (dexFiles == null) { throw new NullPointerException("dexFiles == null"); } if (Arrays.stream(dexFiles).anyMatch(v -> v == null)) { throw new NullPointerException("dexFiles contains a null Buffer!"); } if (dexElements != null || dexElementsSuppressedExceptions != null) { throw new IllegalStateException("Should only be called once"); } final List suppressedExceptions = new ArrayList(); try { Element[] null_elements = null; DexFile dex = new DexFile(dexFiles, definingContext, null_elements); dexElements = new Element[] { new Element(dex) }; } catch (IOException suppressed) { System.logE("Unable to load dex files", suppressed); suppressedExceptions.add(suppressed); dexElements = new Element[0]; } if (suppressedExceptions.size() > 0) { dexElementsSuppressedExceptions = suppressedExceptions.toArray( new IOException[suppressedExceptions.size()]); } } /* package */ void maybeRunBackgroundVerification(ClassLoader loader) { // Spawn background thread to verify all classes and cache verification results. // Must be called *after* `this.dexElements` has been initialized and `loader.pathList` // has been set for ART to find its classes (the fields are hardcoded in ART and dex // files iterated over in the order of the array). // We only spawn the background thread if the bytecode is not backed by an oat // file, i.e. this is the first time this bytecode is being loaded and/or // verification results have not been cached yet. for (Element element : dexElements) { if (element.dexFile != null && !element.dexFile.isBackedByOatFile()) { element.dexFile.verifyInBackground(loader); } } } /** * Splits the given dex path string into elements using the path * separator, pruning out any elements that do not refer to existing * and readable files. */ private static List splitDexPath(String path) { return splitPaths(path, false); } /** * Splits the given path strings into file elements using the path * separator, combining the results and filtering out elements * that don't exist, aren't readable, or aren't either a regular * file or a directory (as specified). Either string may be empty * or {@code null}, in which case it is ignored. If both strings * are empty or {@code null}, or all elements get pruned out, then * this returns a zero-element list. */ @UnsupportedAppUsage private static List splitPaths(String searchPath, boolean directoriesOnly) { List result = new ArrayList<>(); if (searchPath != null) { for (String path : searchPath.split(File.pathSeparator)) { if (directoriesOnly) { try { StructStat sb = Libcore.os.stat(path); if (!S_ISDIR(sb.st_mode)) { continue; } } catch (ErrnoException ignored) { continue; } } result.add(new File(path)); } } return result; } // This method is not used anymore. Kept around only because there are many legacy users of it. @SuppressWarnings("unused") @UnsupportedAppUsage public static Element[] makeInMemoryDexElements(ByteBuffer[] dexFiles, List suppressedExceptions) { Element[] elements = new Element[dexFiles.length]; int elementPos = 0; for (ByteBuffer buf : dexFiles) { try { DexFile dex = new DexFile(new ByteBuffer[] { buf }, /* classLoader */ null, /* dexElements */ null); elements[elementPos++] = new Element(dex); } catch (IOException suppressed) { System.logE("Unable to load dex file: " + buf, suppressed); suppressedExceptions.add(suppressed); } } if (elementPos != elements.length) { elements = Arrays.copyOf(elements, elementPos); } return elements; } /** * Makes an array of dex/resource path elements, one per element of * the given array. */ @UnsupportedAppUsage private static Element[] makeDexElements(List files, File optimizedDirectory, List suppressedExceptions, ClassLoader loader) { return makeDexElements(files, optimizedDirectory, suppressedExceptions, loader, false); } private static Element[] makeDexElements(List files, File optimizedDirectory, List suppressedExceptions, ClassLoader loader, boolean isTrusted) { Element[] elements = new Element[files.size()]; int elementsPos = 0; /* * Open all files and load the (direct or contained) dex files up front. */ for (File file : files) { if (file.isDirectory()) { // We support directories for looking up resources. Looking up resources in // directories is useful for running libcore tests. elements[elementsPos++] = new Element(file); } else if (file.isFile()) { String name = file.getName(); DexFile dex = null; if (name.endsWith(DEX_SUFFIX)) { // Raw dex file (not inside a zip/jar). try { dex = loadDexFile(file, optimizedDirectory, loader, elements); if (dex != null) { elements[elementsPos++] = new Element(dex, null); } } catch (IOException suppressed) { System.logE("Unable to load dex file: " + file, suppressed); suppressedExceptions.add(suppressed); } } else { try { dex = loadDexFile(file, optimizedDirectory, loader, elements); } catch (IOException suppressed) { /* * IOException might get thrown "legitimately" by the DexFile constructor if * the zip file turns out to be resource-only (that is, no classes.dex file * in it). * Let dex == null and hang on to the exception to add to the tea-leaves for * when findClass returns null. */ suppressedExceptions.add(suppressed); } if (dex == null) { elements[elementsPos++] = new Element(file); } else { elements[elementsPos++] = new Element(dex, file); } } if (dex != null && isTrusted) { dex.setTrusted(); } } else { System.logW("ClassLoader referenced unknown path: " + file); } } if (elementsPos != elements.length) { elements = Arrays.copyOf(elements, elementsPos); } return elements; } /** * Constructs a {@code DexFile} instance, as appropriate depending on whether * {@code optimizedDirectory} is {@code null}. An application image file may be associated with * the {@code loader} if it is not null. */ @UnsupportedAppUsage private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader, Element[] elements) throws IOException { if (optimizedDirectory == null) { return new DexFile(file, loader, elements); } else { String optimizedPath = optimizedPathFor(file, optimizedDirectory); return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements); } } /** * Converts a dex/jar file path and an output directory to an * output file path for an associated optimized dex file. */ private static String optimizedPathFor(File path, File optimizedDirectory) { /* * Get the filename component of the path, and replace the * suffix with ".dex" if that's not already the suffix. * * We don't want to use ".odex", because the build system uses * that for files that are paired with resource-only jar * files. If the VM can assume that there's no classes.dex in * the matching jar, it doesn't need to open the jar to check * for updated dependencies, providing a slight performance * boost at startup. The use of ".dex" here matches the use on * files in /data/dalvik-cache. */ String fileName = path.getName(); if (!fileName.endsWith(DEX_SUFFIX)) { int lastDot = fileName.lastIndexOf("."); if (lastDot < 0) { fileName += DEX_SUFFIX; } else { StringBuilder sb = new StringBuilder(lastDot + 4); sb.append(fileName, 0, lastDot); sb.append(DEX_SUFFIX); fileName = sb.toString(); } } File result = new File(optimizedDirectory, fileName); return result.getPath(); } /* * TODO (dimitry): Revert after apps stops relying on the existence of this * method (see http://b/21957414 and http://b/26317852 for details) */ @UnsupportedAppUsage @SuppressWarnings("unused") private static Element[] makePathElements(List files, File optimizedDirectory, List suppressedExceptions) { return makeDexElements(files, optimizedDirectory, suppressedExceptions, null); } /** * Makes an array of directory/zip path elements for the native library search path, one per * element of the given array. */ @UnsupportedAppUsage private static NativeLibraryElement[] makePathElements(List files) { NativeLibraryElement[] elements = new NativeLibraryElement[files.size()]; int elementsPos = 0; for (File file : files) { String path = file.getPath(); if (path.contains(zipSeparator)) { String split[] = path.split(zipSeparator, 2); File zip = new File(split[0]); String dir = split[1]; elements[elementsPos++] = new NativeLibraryElement(zip, dir); } else if (file.isDirectory()) { // We support directories for looking up native libraries. elements[elementsPos++] = new NativeLibraryElement(file); } } if (elementsPos != elements.length) { elements = Arrays.copyOf(elements, elementsPos); } return elements; } /** * Finds the named class in one of the dex files pointed at by * this instance. This will find the one in the earliest listed * path element. If the class is found but has not yet been * defined, then this method will define it in the defining * context that this instance was constructed with. * * @param name of class to find * @param suppressed exceptions encountered whilst finding the class * @return the named class or {@code null} if the class is not * found in any of the dex files */ public Class findClass(String name, List suppressed) { for (Element element : dexElements) { Class clazz = element.findClass(name, definingContext, suppressed); if (clazz != null) { return clazz; } } if (dexElementsSuppressedExceptions != null) { suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions)); } return null; } /** * Finds the named resource in one of the zip/jar files pointed at * by this instance. This will find the one in the earliest listed * path element. * * @return a URL to the named resource or {@code null} if the * resource is not found in any of the zip/jar files */ public URL findResource(String name) { for (Element element : dexElements) { URL url = element.findResource(name); if (url != null) { return url; } } return null; } /** * Finds all the resources with the given name, returning an * enumeration of them. If there are no resources with the given * name, then this method returns an empty enumeration. */ public Enumeration findResources(String name) { ArrayList result = new ArrayList(); for (Element element : dexElements) { URL url = element.findResource(name); if (url != null) { result.add(url); } } return Collections.enumeration(result); } /** * Finds the named native code library on any of the library * directories pointed at by this instance. This will find the * one in the earliest listed directory, ignoring any that are not * readable regular files. * * @return the complete path to the library or {@code null} if no * library was found */ public String findLibrary(String libraryName) { String fileName = System.mapLibraryName(libraryName); for (NativeLibraryElement element : nativeLibraryPathElements) { String path = element.findNativeLibrary(fileName); if (path != null) { return path; } } return null; } /** * Returns the list of all individual dex files paths from the current list. * The list will contain only file paths (i.e. no directories). */ /*package*/ List getDexPaths() { List dexPaths = new ArrayList(); for (Element e : dexElements) { String dexPath = e.getDexPath(); if (dexPath != null) { // Add the element to the list only if it is a file. A null dex path signals the // element is a resource directory or an in-memory dex file. dexPaths.add(dexPath); } } return dexPaths; } /** * Adds a collection of library paths from which to load native libraries. Paths can be absolute * native library directories (i.e. /data/app/foo/lib/arm64) or apk references (i.e. * /data/app/foo/base.apk!/lib/arm64). * * Note: This method will attempt to dedupe elements. * Note: This method replaces the value of {@link #nativeLibraryPathElements} */ @UnsupportedAppUsage public void addNativePath(Collection libPaths) { if (libPaths.isEmpty()) { return; } List libFiles = new ArrayList<>(libPaths.size()); for (String path : libPaths) { libFiles.add(new File(path)); } ArrayList newPaths = new ArrayList<>(nativeLibraryPathElements.length + libPaths.size()); newPaths.addAll(Arrays.asList(nativeLibraryPathElements)); for (NativeLibraryElement element : makePathElements(libFiles)) { if (!newPaths.contains(element)) { newPaths.add(element); } } nativeLibraryPathElements = newPaths.toArray(new NativeLibraryElement[newPaths.size()]); } /** * Element of the dex/resource path. Note: should be called DexElement, but apps reflect on * this. */ /*package*/ static class Element { /** * A file denoting a zip file (in case of a resource jar or a dex jar), or a directory * (only when dexFile is null). */ @UnsupportedAppUsage private final File path; /** Whether {@code path.isDirectory()}, or {@code null} if {@code path == null}. */ private final Boolean pathIsDirectory; @UnsupportedAppUsage private final DexFile dexFile; private ClassPathURLStreamHandler urlHandler; private boolean initialized; /** * Element encapsulates a dex file. This may be a plain dex file (in which case dexZipPath * should be null), or a jar (in which case dexZipPath should denote the zip file). */ @UnsupportedAppUsage public Element(DexFile dexFile, File dexZipPath) { if (dexFile == null && dexZipPath == null) { throw new NullPointerException("Either dexFile or path must be non-null"); } this.dexFile = dexFile; this.path = dexZipPath; // Do any I/O in the constructor so we don't have to do it elsewhere, eg. toString(). this.pathIsDirectory = (path == null) ? null : path.isDirectory(); } public Element(DexFile dexFile) { this(dexFile, null); } public Element(File path) { this(null, path); } /** * Constructor for a bit of backwards compatibility. Some apps use reflection into * internal APIs. Warn, and emulate old behavior if we can. See b/33399341. * * @deprecated The Element class has been split. Use new Element constructors for * classes and resources, and NativeLibraryElement for the library * search path. */ @UnsupportedAppUsage @Deprecated public Element(File dir, boolean isDirectory, File zip, DexFile dexFile) { this(dir != null ? null : dexFile, dir != null ? dir : zip); System.err.println("Warning: Using deprecated Element constructor. Do not use internal" + " APIs, this constructor will be removed in the future."); if (dir != null && (zip != null || dexFile != null)) { throw new IllegalArgumentException("Using dir and zip|dexFile no longer" + " supported."); } if (isDirectory && (zip != null || dexFile != null)) { throw new IllegalArgumentException("Unsupported argument combination."); } } /* * Returns the dex path of this element or null if the element refers to a directory. */ private String getDexPath() { if (path != null) { return path.isDirectory() ? null : path.getAbsolutePath(); } else if (dexFile != null) { // DexFile.getName() returns the path of the dex file. return dexFile.getName(); } return null; } @Override public String toString() { if (dexFile == null) { return (pathIsDirectory ? "directory \"" : "zip file \"") + path + "\""; } else if (path == null) { return "dex file \"" + dexFile + "\""; } else { return "zip file \"" + path + "\""; } } public synchronized void maybeInit() { if (initialized) { return; } if (path == null || pathIsDirectory) { initialized = true; return; } try { // Disable zip path validation for loading APKs as it does not pose a risk of the // zip path traversal vulnerability. urlHandler = new ClassPathURLStreamHandler(path.getPath(), /* enableZipPathValidator */ false); } catch (IOException ioe) { /* * Note: ZipException (a subclass of IOException) * might get thrown by the ZipFile constructor * (e.g. if the file isn't actually a zip/jar * file). */ System.logE("Unable to open zip file: " + path, ioe); urlHandler = null; } // Mark this element as initialized only after we've successfully created // the associated ClassPathURLStreamHandler. That way, we won't leave this // element in an inconsistent state if an exception is thrown during initialization. // // See b/35633614. initialized = true; } public Class findClass(String name, ClassLoader definingContext, List suppressed) { return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed) : null; } public URL findResource(String name) { maybeInit(); if (urlHandler != null) { return urlHandler.getEntryUrlOrNull(name); } // We support directories so we can run tests and/or legacy code // that uses Class.getResource. if (path != null && path.isDirectory()) { File resourceFile = new File(path, name); if (resourceFile.exists()) { try { return resourceFile.toURI().toURL(); } catch (MalformedURLException ex) { throw new RuntimeException(ex); } } } return null; } } /** * Element of the native library path */ /*package*/ static class NativeLibraryElement { /** * A file denoting a directory or zip file. */ @UnsupportedAppUsage private final File path; /** * If path denotes a zip file, this denotes a base path inside the zip. */ private final String zipDir; private ClassPathURLStreamHandler urlHandler; private boolean initialized; @UnsupportedAppUsage public NativeLibraryElement(File dir) { this.path = dir; this.zipDir = null; // We should check whether path is a directory, but that is non-eliminatable overhead. } public NativeLibraryElement(File zip, String zipDir) { this.path = zip; this.zipDir = zipDir; // Simple check that should be able to be eliminated by inlining. We should also // check whether path is a file, but that is non-eliminatable overhead. if (zipDir == null) { throw new IllegalArgumentException(); } } @Override public String toString() { if (zipDir == null) { return "directory \"" + path + "\""; } else { return "zip file \"" + path + "\"" + (!zipDir.isEmpty() ? ", dir \"" + zipDir + "\"" : ""); } } public synchronized void maybeInit() { if (initialized) { return; } if (zipDir == null) { initialized = true; return; } try { // Disable zip path validation for loading APKs as it does not pose a risk of the // zip path traversal vulnerability. urlHandler = new ClassPathURLStreamHandler(path.getPath(), /* enableZipPathValidator */ false); } catch (IOException ioe) { /* * Note: ZipException (a subclass of IOException) * might get thrown by the ZipFile constructor * (e.g. if the file isn't actually a zip/jar * file). */ System.logE("Unable to open zip file: " + path, ioe); urlHandler = null; } // Mark this element as initialized only after we've successfully created // the associated ClassPathURLStreamHandler. That way, we won't leave this // element in an inconsistent state if an exception is thrown during initialization. // // See b/35633614. initialized = true; } public String findNativeLibrary(String name) { maybeInit(); if (zipDir == null) { String entryPath = new File(path, name).getPath(); if (IoUtils.canOpenReadOnly(entryPath)) { return entryPath; } } else if (urlHandler != null) { // Having a urlHandler means the element has a zip file. // In this case Android supports loading the library iff // it is stored in the zip uncompressed. String entryName = zipDir + '/' + name; if (urlHandler.isEntryStored(entryName)) { return path.getPath() + zipSeparator + entryName; } } return null; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof NativeLibraryElement)) return false; NativeLibraryElement that = (NativeLibraryElement) o; return Objects.equals(path, that.path) && Objects.equals(zipDir, that.zipDir); } @Override public int hashCode() { return Objects.hash(path, zipDir); } } }