1 /*
2  * Copyright (C) 2022 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.art;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.os.Build;
22 import android.os.UserHandle;
23 import android.os.UserManager;
24 import android.text.TextUtils;
25 
26 import androidx.annotation.RequiresApi;
27 
28 import com.android.internal.annotations.Immutable;
29 import com.android.server.art.model.DetailedDexInfo;
30 import com.android.server.pm.pkg.AndroidPackage;
31 import com.android.server.pm.pkg.AndroidPackageSplit;
32 import com.android.server.pm.pkg.PackageState;
33 import com.android.server.pm.pkg.PackageUserState;
34 import com.android.server.pm.pkg.SharedLibrary;
35 
36 import dalvik.system.DelegateLastClassLoader;
37 import dalvik.system.DexClassLoader;
38 import dalvik.system.PathClassLoader;
39 
40 import java.io.File;
41 import java.util.ArrayList;
42 import java.util.List;
43 import java.util.Objects;
44 import java.util.function.Predicate;
45 import java.util.stream.Collectors;
46 
47 /** @hide */
48 @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
49 public class PrimaryDexUtils {
50     public static final String PROFILE_PRIMARY = "primary";
51     private static final String SHARED_LIBRARY_LOADER_TYPE = PathClassLoader.class.getName();
52 
53     /**
54      * Returns the basic information about all primary dex files belonging to the package, excluding
55      * files that have no code.
56      */
57     @NonNull
getDexInfo(@onNull AndroidPackage pkg)58     public static List<PrimaryDexInfo> getDexInfo(@NonNull AndroidPackage pkg) {
59         return getDexInfoImpl(pkg)
60                 .stream()
61                 .map(builder -> builder.build())
62                 .filter(info -> info.hasCode())
63                 .collect(Collectors.toList());
64     }
65 
66     /**
67      * Same as above, but requires {@link PackageState} in addition, and returns the detailed
68      * information, including the class loader context.
69      */
70     @NonNull
getDetailedDexInfo( @onNull PackageState pkgState, @NonNull AndroidPackage pkg)71     public static List<DetailedPrimaryDexInfo> getDetailedDexInfo(
72             @NonNull PackageState pkgState, @NonNull AndroidPackage pkg) {
73         return getDetailedDexInfoImpl(pkgState, pkg)
74                 .stream()
75                 .map(builder -> builder.buildDetailed())
76                 .filter(info -> info.hasCode())
77                 .collect(Collectors.toList());
78     }
79 
80     /** Returns the basic information about a dex file specified by {@code splitName}. */
81     @NonNull
getDexInfoBySplitName( @onNull AndroidPackage pkg, @Nullable String splitName)82     public static PrimaryDexInfo getDexInfoBySplitName(
83             @NonNull AndroidPackage pkg, @Nullable String splitName) {
84         PrimaryDexInfo dexInfo =
85                 findDexInfo(pkg, info -> Objects.equals(info.splitName(), splitName));
86         if (dexInfo == null) {
87             throw new IllegalArgumentException(String.format("Split '%s' not found", splitName));
88         }
89         return dexInfo;
90     }
91 
92     /**
93      * Returns the basic information about the first dex file matching {@code predicate}, or null if
94      * not found.
95      */
96     @Nullable
findDexInfo( @onNull AndroidPackage pkg, Predicate<PrimaryDexInfo> predicate)97     public static PrimaryDexInfo findDexInfo(
98             @NonNull AndroidPackage pkg, Predicate<PrimaryDexInfo> predicate) {
99         return getDexInfoImpl(pkg)
100                 .stream()
101                 .map(builder -> builder.build())
102                 .filter(predicate)
103                 .findFirst()
104                 .orElse(null);
105     }
106 
107     @NonNull
getDexInfoImpl(@onNull AndroidPackage pkg)108     private static List<PrimaryDexInfoBuilder> getDexInfoImpl(@NonNull AndroidPackage pkg) {
109         List<PrimaryDexInfoBuilder> dexInfos = new ArrayList<>();
110 
111         for (var split : pkg.getSplits()) {
112             dexInfos.add(new PrimaryDexInfoBuilder(split));
113         }
114 
115         return dexInfos;
116     }
117 
118     @NonNull
getDetailedDexInfoImpl( @onNull PackageState pkgState, @NonNull AndroidPackage pkg)119     private static List<PrimaryDexInfoBuilder> getDetailedDexInfoImpl(
120             @NonNull PackageState pkgState, @NonNull AndroidPackage pkg) {
121         List<PrimaryDexInfoBuilder> dexInfos = getDexInfoImpl(pkg);
122 
123         PrimaryDexInfoBuilder baseApk = dexInfos.get(0);
124         baseApk.mClassLoaderName = baseApk.mSplit.getClassLoaderName();
125         File baseDexFile = new File(baseApk.mSplit.getPath());
126         baseApk.mRelativeDexPath = baseDexFile.getName();
127 
128         // Shared libraries are the dependencies of the base APK.
129         baseApk.mSharedLibrariesContext =
130                 encodeSharedLibraries(pkgState.getSharedLibraryDependencies());
131 
132         boolean isIsolatedSplitLoading = isIsolatedSplitLoading(pkg);
133 
134         for (int i = 1; i < dexInfos.size(); i++) {
135             var dexInfoBuilder = dexInfos.get(i);
136             File splitDexFile = new File(dexInfoBuilder.mSplit.getPath());
137             if (!splitDexFile.getParent().equals(baseDexFile.getParent())) {
138                 throw new IllegalStateException(
139                         "Split APK and base APK are in different directories: "
140                         + splitDexFile.getParent() + " != " + baseDexFile.getParent());
141             }
142             dexInfoBuilder.mRelativeDexPath = splitDexFile.getName();
143             if (isIsolatedSplitLoading && dexInfoBuilder.mSplit.isHasCode()) {
144                 dexInfoBuilder.mClassLoaderName = dexInfoBuilder.mSplit.getClassLoaderName();
145 
146                 List<AndroidPackageSplit> dependencies = dexInfoBuilder.mSplit.getDependencies();
147                 if (!Utils.isEmpty(dependencies)) {
148                     // We only care about the first dependency because it is the parent split. The
149                     // rest are configuration splits, which we don't care.
150                     AndroidPackageSplit dependency = dependencies.get(0);
151                     for (var dexInfo : dexInfos) {
152                         if (Objects.equals(dexInfo.mSplit, dependency)) {
153                             dexInfoBuilder.mSplitDependency = dexInfo;
154                             break;
155                         }
156                     }
157 
158                     if (dexInfoBuilder.mSplitDependency == null) {
159                         throw new IllegalStateException(
160                                 "Split dependency not found for " + splitDexFile);
161                     }
162                 }
163             }
164         }
165 
166         if (isIsolatedSplitLoading) {
167             computeClassLoaderContextsIsolated(dexInfos);
168         } else {
169             computeClassLoaderContexts(dexInfos);
170         }
171 
172         return dexInfos;
173     }
174 
175     /**
176      * Computes class loader context for an app that didn't request isolated split loading. Stores
177      * the results in {@link PrimaryDexInfoBuilder#mClassLoaderContext}.
178      *
179      * In this case, all the splits will be loaded in the base apk class loader (in the order of
180      * their definition).
181      *
182      * The CLC for the base APK is `CLN[]{shared-libraries}`; the CLC for the n-th split APK is
183      * `CLN[base.apk, split_0.apk, ..., split_n-1.apk]{shared-libraries}`; where `CLN` is the
184      * class loader name for the base APK.
185      */
computeClassLoaderContexts(@onNull List<PrimaryDexInfoBuilder> dexInfos)186     private static void computeClassLoaderContexts(@NonNull List<PrimaryDexInfoBuilder> dexInfos) {
187         String baseClassLoaderName = dexInfos.get(0).mClassLoaderName;
188         String sharedLibrariesContext = dexInfos.get(0).mSharedLibrariesContext;
189         List<String> classpath = new ArrayList<>();
190         for (PrimaryDexInfoBuilder dexInfo : dexInfos) {
191             if (dexInfo.mSplit.isHasCode()) {
192                 dexInfo.mClassLoaderContext = encodeClassLoader(baseClassLoaderName, classpath,
193                         null /* parentContext */, sharedLibrariesContext);
194             }
195             // Note that the splits with no code are not removed from the classpath computation.
196             // I.e., split_n might get the split_n-1 in its classpath dependency even if split_n-1
197             // has no code.
198             // The splits with no code do not matter for the runtime which ignores APKs without code
199             // when doing the classpath checks. As such we could actually filter them but we don't
200             // do it in order to keep consistency with how the apps are loaded.
201             classpath.add(dexInfo.mRelativeDexPath);
202         }
203     }
204 
205     /**
206      * Computes class loader context for an app that requested for isolated split loading. Stores
207      * the results in {@link PrimaryDexInfoBuilder#mClassLoaderContext}.
208      *
209      * In this case, each split will be loaded with a separate class loader, whose context is a
210      * chain formed from inter-split dependencies.
211      *
212      * The CLC for the base APK is `CLN[]{shared-libraries}`; the CLC for the n-th split APK that
213      * depends on the base APK is `CLN_n[];CLN[base.apk]{shared-libraries}`; the CLC for the n-th
214      * split APK that depends on the m-th split APK is
215      * `CLN_n[];CLN_m[split_m.apk];...;CLN[base.apk]{shared-libraries}`; where `CLN` is the base
216      * class loader name for the base APK, `CLN_i` is the class loader name for the i-th split APK,
217      * and `...` represents the ancestors along the dependency chain.
218      *
219      * Specially, if a split does not have any dependency, the CLC for it is `CLN_n[]`.
220      */
computeClassLoaderContextsIsolated( @onNull List<PrimaryDexInfoBuilder> dexInfos)221     private static void computeClassLoaderContextsIsolated(
222             @NonNull List<PrimaryDexInfoBuilder> dexInfos) {
223         for (PrimaryDexInfoBuilder dexInfo : dexInfos) {
224             if (dexInfo.mSplit.isHasCode()) {
225                 dexInfo.mClassLoaderContext = encodeClassLoader(dexInfo.mClassLoaderName,
226                         null /* classpath */, getParentContextRecursive(dexInfo),
227                         dexInfo.mSharedLibrariesContext);
228             }
229         }
230     }
231 
232     /**
233      * Computes the parent class loader context, recursively. Caches results in {@link
234      * PrimaryDexInfoBuilder#mContextForChildren}.
235      */
236     @Nullable
getParentContextRecursive(@onNull PrimaryDexInfoBuilder dexInfo)237     private static String getParentContextRecursive(@NonNull PrimaryDexInfoBuilder dexInfo) {
238         if (dexInfo.mSplitDependency == null) {
239             return null;
240         }
241         PrimaryDexInfoBuilder parent = dexInfo.mSplitDependency;
242         if (parent.mContextForChildren == null) {
243             parent.mContextForChildren =
244                     encodeClassLoader(parent.mClassLoaderName, List.of(parent.mRelativeDexPath),
245                             getParentContextRecursive(parent), parent.mSharedLibrariesContext);
246         }
247         return parent.mContextForChildren;
248     }
249 
250     /**
251      * Returns class loader context in the format of
252      * `CLN[classpath...]{share-libraries};parent-context`, where `CLN` is the class loader name.
253      */
254     @NonNull
encodeClassLoader(@ullable String classLoaderName, @Nullable List<String> classpath, @Nullable String parentContext, @Nullable String sharedLibrariesContext)255     private static String encodeClassLoader(@Nullable String classLoaderName,
256             @Nullable List<String> classpath, @Nullable String parentContext,
257             @Nullable String sharedLibrariesContext) {
258         StringBuilder classLoaderContext = new StringBuilder();
259 
260         classLoaderContext.append(encodeClassLoaderName(classLoaderName));
261 
262         classLoaderContext.append(
263                 "[" + (classpath != null ? String.join(":", classpath) : "") + "]");
264 
265         if (!TextUtils.isEmpty(sharedLibrariesContext)) {
266             classLoaderContext.append(sharedLibrariesContext);
267         }
268 
269         if (!TextUtils.isEmpty(parentContext)) {
270             classLoaderContext.append(";" + parentContext);
271         }
272 
273         return classLoaderContext.toString();
274     }
275 
276     @NonNull
encodeClassLoaderName(@ullable String classLoaderName)277     private static String encodeClassLoaderName(@Nullable String classLoaderName) {
278         // `PathClassLoader` and `DexClassLoader` are grouped together because they have the same
279         // behavior. For null values we default to "PCL". This covers the case where a package does
280         // not specify any value for its class loader.
281         if (classLoaderName == null || PathClassLoader.class.getName().equals(classLoaderName)
282                 || DexClassLoader.class.getName().equals(classLoaderName)) {
283             return "PCL";
284         } else if (DelegateLastClassLoader.class.getName().equals(classLoaderName)) {
285             return "DLC";
286         } else {
287             throw new IllegalStateException("Unsupported classLoaderName: " + classLoaderName);
288         }
289     }
290 
291     /**
292      * Returns shared libraries context in the format of
293      * `{PCL[library_1_dex_1.jar:library_1_dex_2.jar:...]{library_1-dependencies}#PCL[
294      *     library_1_dex_2.jar:library_2_dex_2.jar:...]{library_2-dependencies}#...}`.
295      */
296     @Nullable
encodeSharedLibraries(@ullable List<SharedLibrary> sharedLibraries)297     private static String encodeSharedLibraries(@Nullable List<SharedLibrary> sharedLibraries) {
298         if (Utils.isEmpty(sharedLibraries)) {
299             return null;
300         }
301         return sharedLibraries.stream()
302                 .filter(library -> !library.isNative())
303                 .map(library
304                         -> encodeClassLoader(SHARED_LIBRARY_LOADER_TYPE, library.getAllCodePaths(),
305                                 null /* parentContext */,
306                                 encodeSharedLibraries(library.getDependencies())))
307                 .collect(Collectors.joining("#", "{", "}"));
308     }
309 
isIsolatedSplitLoading(@onNull AndroidPackage pkg)310     public static boolean isIsolatedSplitLoading(@NonNull AndroidPackage pkg) {
311         return pkg.isIsolatedSplitLoading() && pkg.getSplits().size() > 1;
312     }
313 
314     @NonNull
buildRefProfilePathAsInput( @onNull PackageState pkgState, @NonNull PrimaryDexInfo dexInfo)315     public static ProfilePath buildRefProfilePathAsInput(
316             @NonNull PackageState pkgState, @NonNull PrimaryDexInfo dexInfo) {
317         String profileName = getProfileName(dexInfo.splitName());
318         return AidlUtils.buildProfilePathForPrimaryRefAsInput(
319                 pkgState.getPackageName(), profileName);
320     }
321 
322     @NonNull
buildOutputProfile(@onNull PackageState pkgState, @NonNull PrimaryDexInfo dexInfo, int uid, int gid, boolean isPublic, boolean isPreReboot)323     public static OutputProfile buildOutputProfile(@NonNull PackageState pkgState,
324             @NonNull PrimaryDexInfo dexInfo, int uid, int gid, boolean isPublic,
325             boolean isPreReboot) {
326         String profileName = getProfileName(dexInfo.splitName());
327         return AidlUtils.buildOutputProfileForPrimary(
328                 pkgState.getPackageName(), profileName, uid, gid, isPublic, isPreReboot);
329     }
330 
331     @NonNull
getCurProfiles(@onNull UserManager userManager, @NonNull PackageState pkgState, @NonNull PrimaryDexInfo dexInfo)332     public static List<ProfilePath> getCurProfiles(@NonNull UserManager userManager,
333             @NonNull PackageState pkgState, @NonNull PrimaryDexInfo dexInfo) {
334         return getCurProfiles(
335                 userManager.getUserHandles(true /* excludeDying */), pkgState, dexInfo);
336     }
337 
338     @NonNull
getCurProfiles(@onNull List<UserHandle> userHandles, @NonNull PackageState pkgState, @NonNull PrimaryDexInfo dexInfo)339     public static List<ProfilePath> getCurProfiles(@NonNull List<UserHandle> userHandles,
340             @NonNull PackageState pkgState, @NonNull PrimaryDexInfo dexInfo) {
341         List<ProfilePath> profiles = new ArrayList<>();
342         for (UserHandle handle : userHandles) {
343             int userId = handle.getIdentifier();
344             PackageUserState userState = pkgState.getStateForUser(handle);
345             if (userState.isInstalled()) {
346                 profiles.add(AidlUtils.buildProfilePathForPrimaryCur(
347                         userId, pkgState.getPackageName(), getProfileName(dexInfo.splitName())));
348             }
349         }
350         return profiles;
351     }
352 
353     @NonNull
getProfileName(@ullable String splitName)354     public static String getProfileName(@Nullable String splitName) {
355         return splitName == null ? PROFILE_PRIMARY : splitName + ".split";
356     }
357 
358     @NonNull
getExternalProfiles(@onNull PrimaryDexInfo dexInfo)359     public static List<ProfilePath> getExternalProfiles(@NonNull PrimaryDexInfo dexInfo) {
360         return List.of(AidlUtils.buildProfilePathForPrebuilt(dexInfo.dexPath()),
361                 AidlUtils.buildProfilePathForDm(dexInfo.dexPath()));
362     }
363 
364     /** Basic information about a primary dex file (either the base APK or a split APK). */
365     @Immutable
366     public static class PrimaryDexInfo {
367         private final @NonNull AndroidPackageSplit mSplit;
368 
PrimaryDexInfo(@onNull AndroidPackageSplit split)369         PrimaryDexInfo(@NonNull AndroidPackageSplit split) {
370             mSplit = split;
371         }
372 
373         /** The path to the dex file. */
dexPath()374         public @NonNull String dexPath() {
375             return mSplit.getPath();
376         }
377 
378         /** True if the dex file has code. */
hasCode()379         public boolean hasCode() {
380             return mSplit.isHasCode();
381         }
382 
383         /** The name of the split, or null for base APK. */
splitName()384         public @Nullable String splitName() {
385             return mSplit.getName();
386         }
387     }
388 
389     /**
390      * Detailed information about a primary dex file (either the base APK or a split APK). It
391      * contains the class loader context in addition to what is in {@link PrimaryDexInfo}, but
392      * producing it requires {@link PackageState}.
393      */
394     @Immutable
395     public static class DetailedPrimaryDexInfo extends PrimaryDexInfo implements DetailedDexInfo {
396         private final @Nullable String mClassLoaderContext;
397 
DetailedPrimaryDexInfo( @onNull AndroidPackageSplit split, @Nullable String classLoaderContext)398         DetailedPrimaryDexInfo(
399                 @NonNull AndroidPackageSplit split, @Nullable String classLoaderContext) {
400             super(split);
401             mClassLoaderContext = classLoaderContext;
402         }
403 
404         /**
405          * A string describing the structure of the class loader that the dex file is loaded with.
406          */
classLoaderContext()407         public @Nullable String classLoaderContext() {
408             return mClassLoaderContext;
409         }
410     }
411 
412     private static class PrimaryDexInfoBuilder {
413         @NonNull AndroidPackageSplit mSplit;
414         @Nullable String mRelativeDexPath = null;
415         @Nullable String mClassLoaderContext = null;
416         @Nullable String mClassLoaderName = null;
417         @Nullable PrimaryDexInfoBuilder mSplitDependency = null;
418         /** The class loader context of the shared libraries. Only applicable for the base APK. */
419         @Nullable String mSharedLibrariesContext = null;
420         /** The class loader context for children to use when this dex file is used as a parent. */
421         @Nullable String mContextForChildren = null;
422 
PrimaryDexInfoBuilder(@onNull AndroidPackageSplit split)423         PrimaryDexInfoBuilder(@NonNull AndroidPackageSplit split) {
424             mSplit = split;
425         }
426 
build()427         PrimaryDexInfo build() {
428             return new PrimaryDexInfo(mSplit);
429         }
430 
buildDetailed()431         DetailedPrimaryDexInfo buildDetailed() {
432             return new DetailedPrimaryDexInfo(mSplit, mClassLoaderContext);
433         }
434     }
435 }
436