1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package com.android.server.pm.dex;
18 
19 import android.content.pm.ApplicationInfo;
20 import android.content.pm.SharedLibraryInfo;
21 import android.util.Slog;
22 import android.util.SparseArray;
23 
24 import com.android.internal.os.ClassLoaderFactory;
25 
26 import java.io.File;
27 import java.util.List;
28 
29 public final class DexoptUtils {
30     private static final String TAG = "DexoptUtils";
31 
32     // Shared libraries have more or less followed PCL behavior due to the way
33     // they were added to the classpath pre Q.
34     private static final String SHARED_LIBRARY_LOADER_TYPE =
35             ClassLoaderFactory.getPathClassLoaderName();
36 
DexoptUtils()37     private DexoptUtils() {}
38 
39     /**
40      * Creates the class loader context dependencies for each of the application code paths.
41      * The returned array contains the class loader contexts that needs to be passed to dexopt in
42      * order to ensure correct optimizations. "Code" paths with no actual code, as specified by
43      * {@param pathsWithCode}, are ignored and will have null as their context in the returned array
44      * (configuration splits are an example of paths without code).
45      *
46      * A class loader context describes how the class loader chain should be built by dex2oat
47      * in order to ensure that classes are resolved during compilation as they would be resolved
48      * at runtime. The context will be encoded in the compiled code. If at runtime the dex file is
49      * loaded in a different context (with a different set of class loaders or a different
50      * classpath), the compiled code will be rejected.
51      *
52      * Note that the class loader context only includes dependencies and not the code path itself.
53      * The contexts are created based on the application split dependency list and
54      * the provided shared libraries.
55      *
56      * All the code paths encoded in the context will be relative to the base directory. This
57      * enables stage compilation where compiler artifacts may be moved around.
58      *
59      * The result is indexed as follows:
60      *   - index 0 contains the context for the base apk
61      *   - index 1 to n contain the context for the splits in the order determined by
62      *     {@code info.getSplitCodePaths()}
63      *
64      * IMPORTANT: keep this logic in sync with the loading code in {@link android.app.LoadedApk}
65      * and pay attention to the way the classpath is created for the non isolated mode in:
66      * {@link android.app.LoadedApk#makePaths(
67      * android.app.ActivityThread, boolean, ApplicationInfo, List, List)}.
68      */
getClassLoaderContexts(ApplicationInfo info, List<SharedLibraryInfo> sharedLibraries, boolean[] pathsWithCode)69     public static String[] getClassLoaderContexts(ApplicationInfo info,
70             List<SharedLibraryInfo> sharedLibraries, boolean[] pathsWithCode) {
71         // The base class loader context contains only the shared library.
72         String sharedLibrariesContext = "";
73         if (sharedLibraries != null) {
74             sharedLibrariesContext = encodeSharedLibraries(sharedLibraries);
75         }
76 
77         String baseApkContextClassLoader = encodeClassLoader(
78                 "", info.classLoaderName, sharedLibrariesContext);
79         if (info.getSplitCodePaths() == null) {
80             // The application has no splits.
81             return new String[] {baseApkContextClassLoader};
82         }
83 
84         // The application has splits. Compute their class loader contexts.
85 
86         // First, cache the relative paths of the splits and do some sanity checks
87         String[] splitRelativeCodePaths = getSplitRelativeCodePaths(info);
88 
89         // The splits have an implicit dependency on the base apk.
90         // This means that we have to add the base apk file in addition to the shared libraries.
91         String baseApkName = new File(info.getBaseCodePath()).getName();
92         String baseClassPath = baseApkName;
93 
94         // The result is stored in classLoaderContexts.
95         // Index 0 is the class loader context for the base apk.
96         // Index `i` is the class loader context encoding for split `i`.
97         String[] classLoaderContexts = new String[/*base apk*/ 1 + splitRelativeCodePaths.length];
98         classLoaderContexts[0] = pathsWithCode[0] ? baseApkContextClassLoader : null;
99 
100         if (!info.requestsIsolatedSplitLoading() || info.splitDependencies == null) {
101             // If the app didn't request for the splits to be loaded in isolation or if it does not
102             // declare inter-split dependencies, then all the splits will be loaded in the base
103             // apk class loader (in the order of their definition).
104             String classpath = baseClassPath;
105             for (int i = 1; i < classLoaderContexts.length; i++) {
106                 if (pathsWithCode[i]) {
107                     classLoaderContexts[i] = encodeClassLoader(
108                             classpath, info.classLoaderName, sharedLibrariesContext);
109                 } else {
110                     classLoaderContexts[i] = null;
111                 }
112                 // Note that the splits with no code are not removed from the classpath computation.
113                 // i.e. split_n might get the split_n-1 in its classpath dependency even
114                 // if split_n-1 has no code.
115                 // The splits with no code do not matter for the runtime which ignores
116                 // apks without code when doing the classpath checks. As such we could actually
117                 // filter them but we don't do it in order to keep consistency with how the apps
118                 // are loaded.
119                 classpath = encodeClasspath(classpath, splitRelativeCodePaths[i - 1]);
120             }
121         } else {
122             // In case of inter-split dependencies, we need to walk the dependency chain of each
123             // split. We do this recursively and store intermediate results in classLoaderContexts.
124 
125             // First, look at the split class loaders and cache their individual contexts (i.e.
126             // the class loader + the name of the split). This is an optimization to avoid
127             // re-computing them during the recursive call.
128             // The cache is stored in splitClassLoaderEncodingCache. The difference between this and
129             // classLoaderContexts is that the later contains the full chain of class loaders for
130             // a given split while splitClassLoaderEncodingCache only contains a single class loader
131             // encoding.
132             String[] splitClassLoaderEncodingCache = new String[splitRelativeCodePaths.length];
133             for (int i = 0; i < splitRelativeCodePaths.length; i++) {
134                 splitClassLoaderEncodingCache[i] = encodeClassLoader(splitRelativeCodePaths[i],
135                         info.splitClassLoaderNames[i]);
136             }
137             String splitDependencyOnBase = encodeClassLoader(
138                     baseClassPath, info.classLoaderName);
139             SparseArray<int[]> splitDependencies = info.splitDependencies;
140 
141             // Note that not all splits have dependencies (e.g. configuration splits)
142             // The splits without dependencies will have classLoaderContexts[config_split_index]
143             // set to null after this step.
144             for (int i = 1; i < splitDependencies.size(); i++) {
145                 int splitIndex = splitDependencies.keyAt(i);
146                 if (pathsWithCode[splitIndex]) {
147                     // Compute the class loader context only for the splits with code.
148                     getParentDependencies(splitIndex, splitClassLoaderEncodingCache,
149                             splitDependencies, classLoaderContexts, splitDependencyOnBase);
150                 }
151             }
152 
153             // At this point classLoaderContexts contains only the parent dependencies.
154             // We also need to add the class loader of the current split which should
155             // come first in the context.
156             for (int i = 1; i < classLoaderContexts.length; i++) {
157                 String splitClassLoader = encodeClassLoader("", info.splitClassLoaderNames[i - 1]);
158                 if (pathsWithCode[i]) {
159                     // If classLoaderContexts[i] is null it means that the split does not have
160                     // any dependency. In this case its context equals its declared class loader.
161                     classLoaderContexts[i] = classLoaderContexts[i] == null
162                             ? splitClassLoader
163                             : encodeClassLoaderChain(splitClassLoader, classLoaderContexts[i])
164                                     + sharedLibrariesContext;
165                 } else {
166                     // This is a split without code, it has no dependency and it is not compiled.
167                     // Its context will be null.
168                     classLoaderContexts[i] = null;
169                 }
170             }
171         }
172 
173         return classLoaderContexts;
174     }
175 
176     /**
177      * Creates the class loader context for the given shared library.
178      */
getClassLoaderContext(SharedLibraryInfo info)179     public static String getClassLoaderContext(SharedLibraryInfo info) {
180         String sharedLibrariesContext = "";
181         if (info.getDependencies() != null) {
182             sharedLibrariesContext = encodeSharedLibraries(info.getDependencies());
183         }
184         return encodeClassLoader(
185                 "", SHARED_LIBRARY_LOADER_TYPE, sharedLibrariesContext);
186     }
187 
188     /**
189      * Recursive method to generate the class loader context dependencies for the split with the
190      * given index. {@param classLoaderContexts} acts as an accumulator. Upton return
191      * {@code classLoaderContexts[index]} will contain the split dependency.
192      * During computation, the method may resolve the dependencies of other splits as it traverses
193      * the entire parent chain. The result will also be stored in {@param classLoaderContexts}.
194      *
195      * Note that {@code index 0} denotes the base apk and it is special handled. When the
196      * recursive call hits {@code index 0} the method returns {@code splitDependencyOnBase}.
197      * {@code classLoaderContexts[0]} is not modified in this method.
198      *
199      * @param index the index of the split (Note that index 0 denotes the base apk)
200      * @param splitClassLoaderEncodingCache the class loader encoding for the individual splits.
201      *    It contains only the split class loader and not the the base. The split
202      *    with {@code index} has its context at {@code splitClassLoaderEncodingCache[index - 1]}.
203      * @param splitDependencies the dependencies for all splits. Note that in this array index 0
204      *    is the base and splits start from index 1.
205      * @param classLoaderContexts the result accumulator. index 0 is the base and never set. Splits
206      *    start at index 1.
207      * @param splitDependencyOnBase the encoding of the implicit split dependency on base.
208      */
getParentDependencies(int index, String[] splitClassLoaderEncodingCache, SparseArray<int[]> splitDependencies, String[] classLoaderContexts, String splitDependencyOnBase)209     private static String getParentDependencies(int index, String[] splitClassLoaderEncodingCache,
210             SparseArray<int[]> splitDependencies, String[] classLoaderContexts,
211             String splitDependencyOnBase) {
212         // If we hit the base apk return its custom dependency list which is
213         // sharedLibraries + base.apk
214         if (index == 0) {
215             return splitDependencyOnBase;
216         }
217         // Return the result if we've computed the splitDependencies for this index already.
218         if (classLoaderContexts[index] != null) {
219             return classLoaderContexts[index];
220         }
221         // Get the splitDependencies for the parent of this index and append its path to it.
222         int parent = splitDependencies.get(index)[0];
223         String parentDependencies = getParentDependencies(parent, splitClassLoaderEncodingCache,
224                 splitDependencies, classLoaderContexts, splitDependencyOnBase);
225 
226         // The split context is: `parent context + parent dependencies context`.
227         String splitContext = (parent == 0) ?
228                 parentDependencies :
229                 encodeClassLoaderChain(splitClassLoaderEncodingCache[parent - 1], parentDependencies);
230         classLoaderContexts[index] = splitContext;
231         return splitContext;
232     }
233 
encodeSharedLibrary(SharedLibraryInfo sharedLibrary)234     private static String encodeSharedLibrary(SharedLibraryInfo sharedLibrary) {
235         List<String> paths = sharedLibrary.getAllCodePaths();
236         String classLoaderSpec = encodeClassLoader(
237                 encodeClasspath(paths.toArray(new String[paths.size()])),
238                 SHARED_LIBRARY_LOADER_TYPE);
239         if (sharedLibrary.getDependencies() != null) {
240             classLoaderSpec += encodeSharedLibraries(sharedLibrary.getDependencies());
241         }
242         return classLoaderSpec;
243     }
244 
encodeSharedLibraries(List<SharedLibraryInfo> sharedLibraries)245     private static String encodeSharedLibraries(List<SharedLibraryInfo> sharedLibraries) {
246         String sharedLibrariesContext = "{";
247         boolean first = true;
248         for (SharedLibraryInfo info : sharedLibraries) {
249             if (!first) {
250                 sharedLibrariesContext += "#";
251             }
252             first = false;
253             sharedLibrariesContext += encodeSharedLibrary(info);
254         }
255         sharedLibrariesContext += "}";
256         return sharedLibrariesContext;
257     }
258 
259     /**
260      * Encodes the shared libraries classpathElements in a format accepted by dexopt.
261      * NOTE: Keep this in sync with the dexopt expectations! Right now that is
262      * a list separated by ':'.
263      */
encodeClasspath(String[] classpathElements)264     private static String encodeClasspath(String[] classpathElements) {
265         if (classpathElements == null || classpathElements.length == 0) {
266             return "";
267         }
268         StringBuilder sb = new StringBuilder();
269         for (String element : classpathElements) {
270             if (sb.length() != 0) {
271                 sb.append(":");
272             }
273             sb.append(element);
274         }
275         return sb.toString();
276     }
277 
278     /**
279      * Adds an element to the encoding of an existing classpath.
280      * {@see PackageDexOptimizer.encodeClasspath(String[])}
281      */
encodeClasspath(String classpath, String newElement)282     private static String encodeClasspath(String classpath, String newElement) {
283         return classpath.isEmpty() ? newElement : (classpath + ":" + newElement);
284     }
285 
286     /**
287      * Encodes a single class loader dependency starting from {@param path} and
288      * {@param classLoaderName}.
289      * NOTE: Keep this in sync with the dexopt expectations! Right now that is either "PCL[path]"
290      * for a PathClassLoader or "DLC[path]" for a DelegateLastClassLoader.
291      */
encodeClassLoader(String classpath, String classLoaderName)292     /*package*/ static String encodeClassLoader(String classpath, String classLoaderName) {
293         classpath.getClass();  // Throw NPE if classpath is null
294         String classLoaderDexoptEncoding = classLoaderName;
295         if (ClassLoaderFactory.isPathClassLoaderName(classLoaderName)) {
296             classLoaderDexoptEncoding = "PCL";
297         } else if (ClassLoaderFactory.isDelegateLastClassLoaderName(classLoaderName)) {
298             classLoaderDexoptEncoding = "DLC";
299         } else {
300             Slog.wtf(TAG, "Unsupported classLoaderName: " + classLoaderName);
301         }
302         return classLoaderDexoptEncoding + "[" + classpath + "]";
303     }
304 
305     /**
306      * Same as above, but appends {@param sharedLibraries} to the result.
307      */
encodeClassLoader(String classpath, String classLoaderName, String sharedLibraries)308     private static String encodeClassLoader(String classpath, String classLoaderName,
309             String sharedLibraries) {
310         return encodeClassLoader(classpath, classLoaderName) + sharedLibraries;
311     }
312 
313     /**
314      * Links to dependencies together in a format accepted by dexopt.
315      * For the special case when either of cl1 or cl2 equals
316      * NOTE: Keep this in sync with the dexopt expectations! Right now that is a list of split
317      * dependencies {@see encodeClassLoader} separated by ';'.
318      */
encodeClassLoaderChain(String cl1, String cl2)319     /*package*/ static String encodeClassLoaderChain(String cl1, String cl2) {
320         if (cl1.isEmpty()) return cl2;
321         if (cl2.isEmpty()) return cl1;
322         return cl1 + ";" + cl2;
323     }
324 
325     /**
326      * Compute the class loader context for the dex files present in the classpath of the first
327      * class loader from the given list (referred in the code as the {@code loadingClassLoader}).
328      * Each dex files gets its own class loader context in the returned array.
329      *
330      * Example:
331      *    If classLoadersNames = {"dalvik.system.DelegateLastClassLoader",
332      *    "dalvik.system.PathClassLoader"} and classPaths = {"foo.dex:bar.dex", "other.dex"}
333      *    The output will be
334      *    {"DLC[];PCL[other.dex]", "DLC[foo.dex];PCL[other.dex]"}
335      *    with "DLC[];PCL[other.dex]" being the context for "foo.dex"
336      *    and "DLC[foo.dex];PCL[other.dex]" the context for "bar.dex".
337      *
338      * If any of the class loaders names is unsupported the method will return null.
339      *
340      * The argument lists must be non empty and of the same size.
341      *
342      * @param classLoadersNames the names of the class loaders present in the loading chain. The
343      *    list encodes the class loader chain in the natural order. The first class loader has
344      *    the second one as its parent and so on.
345      * @param classPaths the class paths for the elements of {@param classLoadersNames}. The
346      *     the first element corresponds to the first class loader and so on. A classpath is
347      *     represented as a list of dex files separated by {@code File.pathSeparator}.
348      *     The return context will be for the dex files found in the first class path.
349      */
processContextForDexLoad(List<String> classLoadersNames, List<String> classPaths)350     /*package*/ static String[] processContextForDexLoad(List<String> classLoadersNames,
351             List<String> classPaths) {
352         if (classLoadersNames.size() != classPaths.size()) {
353             throw new IllegalArgumentException(
354                     "The size of the class loader names and the dex paths do not match.");
355         }
356         if (classLoadersNames.isEmpty()) {
357             throw new IllegalArgumentException("Empty classLoadersNames");
358         }
359 
360         // Compute the context for the parent class loaders.
361         String parentContext = "";
362         // We know that these lists are actually ArrayLists so getting the elements by index
363         // is fine (they come over binder). Even if something changes we expect the sizes to be
364         // very small and it shouldn't matter much.
365         for (int i = 1; i < classLoadersNames.size(); i++) {
366             if (!ClassLoaderFactory.isValidClassLoaderName(classLoadersNames.get(i))
367                 || classPaths.get(i) == null) {
368                 return null;
369             }
370             String classpath = encodeClasspath(classPaths.get(i).split(File.pathSeparator));
371             parentContext = encodeClassLoaderChain(parentContext,
372                     encodeClassLoader(classpath, classLoadersNames.get(i)));
373         }
374 
375         // Now compute the class loader context for each dex file from the first classpath.
376         String loadingClassLoader = classLoadersNames.get(0);
377         if (!ClassLoaderFactory.isValidClassLoaderName(loadingClassLoader)) {
378             return null;
379         }
380         String[] loadedDexPaths = classPaths.get(0).split(File.pathSeparator);
381         String[] loadedDexPathsContext = new String[loadedDexPaths.length];
382         String currentLoadedDexPathClasspath = "";
383         for (int i = 0; i < loadedDexPaths.length; i++) {
384             String dexPath = loadedDexPaths[i];
385             String currentContext = encodeClassLoader(
386                     currentLoadedDexPathClasspath, loadingClassLoader);
387             loadedDexPathsContext[i] = encodeClassLoaderChain(currentContext, parentContext);
388             currentLoadedDexPathClasspath = encodeClasspath(currentLoadedDexPathClasspath, dexPath);
389         }
390         return loadedDexPathsContext;
391     }
392 
393     /**
394      * Returns the relative paths of the splits declared by the application {@code info}.
395      * Assumes that the application declares a non-null array of splits.
396      */
getSplitRelativeCodePaths(ApplicationInfo info)397     private static String[] getSplitRelativeCodePaths(ApplicationInfo info) {
398         String baseCodePath = new File(info.getBaseCodePath()).getParent();
399         String[] splitCodePaths = info.getSplitCodePaths();
400         String[] splitRelativeCodePaths = new String[splitCodePaths.length];
401         for (int i = 0; i < splitCodePaths.length; i++) {
402             File pathFile = new File(splitCodePaths[i]);
403             splitRelativeCodePaths[i] = pathFile.getName();
404             // Sanity check that the base paths of the splits are all the same.
405             String basePath = pathFile.getParent();
406             if (!basePath.equals(baseCodePath)) {
407                 Slog.wtf(TAG, "Split paths have different base paths: " + basePath + " and " +
408                         baseCodePath);
409             }
410         }
411         return splitRelativeCodePaths;
412     }
413 }
414