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