1 /* 2 * Copyright (C) 2020 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.parsing; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.content.pm.PackageParserCacheHelper; 22 import android.os.Environment; 23 import android.os.FileUtils; 24 import android.os.Parcel; 25 import android.system.ErrnoException; 26 import android.system.Os; 27 import android.system.OsConstants; 28 import android.system.StructStat; 29 import android.util.Slog; 30 31 import com.android.internal.annotations.VisibleForTesting; 32 import com.android.internal.pm.parsing.IPackageCacher; 33 import com.android.internal.pm.parsing.PackageParser2; 34 import com.android.internal.pm.parsing.pkg.PackageImpl; 35 import com.android.internal.pm.parsing.pkg.ParsedPackage; 36 import com.android.internal.pm.pkg.component.AconfigFlags; 37 import com.android.internal.pm.pkg.parsing.ParsingPackageUtils; 38 import com.android.server.pm.ApexManager; 39 40 import libcore.io.IoUtils; 41 42 import java.io.File; 43 import java.io.FileOutputStream; 44 import java.io.IOException; 45 import java.util.Map; 46 import java.util.Objects; 47 import java.util.concurrent.atomic.AtomicInteger; 48 49 public class PackageCacher implements IPackageCacher { 50 51 private static final String TAG = "PackageCacher"; 52 53 /** 54 * Total number of packages that were read from the cache. We use it only for logging. 55 */ 56 public static final AtomicInteger sCachedPackageReadCount = new AtomicInteger(); 57 58 @NonNull 59 private final File mCacheDir; 60 @Nullable 61 private final PackageParser2.Callback mCallback; 62 63 private static final AconfigFlags sAconfigFlags = ParsingPackageUtils.getAconfigFlags(); 64 PackageCacher(File cacheDir)65 public PackageCacher(File cacheDir) { 66 this(cacheDir, null); 67 } 68 PackageCacher(File cacheDir, @Nullable PackageParser2.Callback callback)69 public PackageCacher(File cacheDir, @Nullable PackageParser2.Callback callback) { 70 this.mCacheDir = cacheDir; 71 this.mCallback = callback; 72 } 73 74 /** 75 * Returns the cache key for a specified {@code packageFile} and {@code flags}. 76 */ getCacheKey(File packageFile, int flags)77 private String getCacheKey(File packageFile, int flags) { 78 StringBuilder sb = new StringBuilder(packageFile.getName()); 79 sb.append('-'); 80 sb.append(flags); 81 sb.append('-'); 82 sb.append(packageFile.getAbsolutePath().hashCode()); 83 84 return sb.toString(); 85 } 86 87 @VisibleForTesting fromCacheEntry(byte[] bytes)88 protected ParsedPackage fromCacheEntry(byte[] bytes) { 89 return fromCacheEntryStatic(bytes, mCallback); 90 } 91 92 /** static version of {@link #fromCacheEntry} for unit tests. */ 93 @VisibleForTesting fromCacheEntryStatic(byte[] bytes)94 public static ParsedPackage fromCacheEntryStatic(byte[] bytes) { 95 return fromCacheEntryStatic(bytes, null); 96 } 97 fromCacheEntryStatic(byte[] bytes, @Nullable ParsingPackageUtils.Callback callback)98 private static ParsedPackage fromCacheEntryStatic(byte[] bytes, 99 @Nullable ParsingPackageUtils.Callback callback) { 100 final Parcel p = Parcel.obtain(); 101 p.unmarshall(bytes, 0, bytes.length); 102 p.setDataPosition(0); 103 104 final PackageParserCacheHelper.ReadHelper helper = 105 new PackageParserCacheHelper.ReadHelper(p); 106 helper.startAndInstall(); 107 108 ParsedPackage pkg = new PackageImpl(p, callback); 109 110 p.recycle(); 111 112 sCachedPackageReadCount.incrementAndGet(); 113 114 return pkg; 115 } 116 117 @VisibleForTesting toCacheEntry(ParsedPackage pkg)118 protected byte[] toCacheEntry(ParsedPackage pkg) { 119 return toCacheEntryStatic(pkg); 120 121 } 122 123 /** static version of {@link #toCacheEntry} for unit tests. */ 124 @VisibleForTesting toCacheEntryStatic(ParsedPackage pkg)125 public static byte[] toCacheEntryStatic(ParsedPackage pkg) { 126 final Parcel p = Parcel.obtain(); 127 final PackageParserCacheHelper.WriteHelper helper = 128 new PackageParserCacheHelper.WriteHelper(p); 129 130 ((PackageImpl) pkg).writeToParcel(p, 0 /* flags */); 131 132 helper.finishAndUninstall(); 133 134 byte[] serialized = p.marshall(); 135 p.recycle(); 136 137 return serialized; 138 } 139 140 /** 141 * Given a {@code packageFile} and a {@code cacheFile} returns whether the 142 * cache file is up to date based on the mod-time of both files. 143 */ isCacheFileUpToDate(File packageFile, File cacheFile)144 private static boolean isCacheFileUpToDate(File packageFile, File cacheFile) { 145 try { 146 // In case packageFile is located on one of /apex mount points it's mtime will always be 147 // 0. Instead, we can use mtime of the APEX file backing the corresponding mount point. 148 if (packageFile.toPath().startsWith(Environment.getApexDirectory().toPath())) { 149 File backingApexFile = ApexManager.getInstance().getBackingApexFile(packageFile); 150 if (backingApexFile == null) { 151 Slog.w(TAG, 152 "Failed to find APEX file backing " + packageFile.getAbsolutePath()); 153 } else { 154 packageFile = backingApexFile; 155 } 156 } 157 // NOTE: We don't use the File.lastModified API because it has the very 158 // non-ideal failure mode of returning 0 with no excepions thrown. 159 // The nio2 Files API is a little better but is considerably more expensive. 160 final StructStat pkg = Os.stat(packageFile.getAbsolutePath()); 161 final StructStat cache = Os.stat(cacheFile.getAbsolutePath()); 162 return pkg.st_mtime < cache.st_mtime; 163 } catch (ErrnoException ee) { 164 // The most common reason why stat fails is that a given cache file doesn't 165 // exist. We ignore that here. It's easy to reason that it's safe to say the 166 // cache isn't up to date if we see any sort of exception here. 167 // 168 // (1) Exception while stating the package file : This should never happen, 169 // and if it does, we do a full package parse (which is likely to throw the 170 // same exception). 171 // (2) Exception while stating the cache file : If the file doesn't exist, the 172 // cache is obviously out of date. If the file *does* exist, we can't read it. 173 // We will attempt to delete and recreate it after parsing the package. 174 if (ee.errno != OsConstants.ENOENT) { 175 Slog.w("Error while stating package cache : ", ee); 176 } 177 178 return false; 179 } 180 } 181 182 /** 183 * Returns the cached parse result for {@code packageFile} for parse flags {@code flags}, 184 * or {@code null} if no cached result exists. 185 */ 186 @Override getCachedResult(File packageFile, int flags)187 public ParsedPackage getCachedResult(File packageFile, int flags) { 188 final String cacheKey = getCacheKey(packageFile, flags); 189 final File cacheFile = new File(mCacheDir, cacheKey); 190 191 try { 192 // If the cache is not up to date, return null. 193 if (!isCacheFileUpToDate(packageFile, cacheFile)) { 194 return null; 195 } 196 197 final byte[] bytes = IoUtils.readFileAsByteArray(cacheFile.getAbsolutePath()); 198 final ParsedPackage parsed = fromCacheEntry(bytes); 199 if (!packageFile.getAbsolutePath().equals(parsed.getPath())) { 200 // Don't use this cache if the path doesn't match 201 return null; 202 } 203 204 if (!android.content.pm.Flags.includeFeatureFlagsInPackageCacher()) { 205 return parsed; 206 } 207 208 final Map<String, Boolean> featureFlagState = 209 ((PackageImpl) parsed).getFeatureFlagState(); 210 if (!featureFlagState.isEmpty()) { 211 Slog.d(TAG, "Feature flags for package " + packageFile + ": " + featureFlagState); 212 for (var entry : featureFlagState.entrySet()) { 213 final String flagPackageAndName = entry.getKey(); 214 if (!Objects.equals(sAconfigFlags.getFlagValue(flagPackageAndName), 215 entry.getValue())) { 216 Slog.i(TAG, "Feature flag " + flagPackageAndName + " changed for package " 217 + packageFile + "; cached result is invalid"); 218 return null; 219 } 220 } 221 } 222 223 return parsed; 224 } catch (Throwable e) { 225 Slog.w(TAG, "Error reading package cache: ", e); 226 227 // If something went wrong while reading the cache entry, delete the cache file 228 // so that we regenerate it the next time. 229 cacheFile.delete(); 230 return null; 231 } 232 } 233 234 /** 235 * Caches the parse result for {@code packageFile} with flags {@code flags}. 236 */ 237 @Override cacheResult(File packageFile, int flags, ParsedPackage parsed)238 public void cacheResult(File packageFile, int flags, ParsedPackage parsed) { 239 try { 240 final String cacheKey = getCacheKey(packageFile, flags); 241 final File cacheFile = new File(mCacheDir, cacheKey); 242 243 if (cacheFile.exists()) { 244 if (!cacheFile.delete()) { 245 Slog.e(TAG, "Unable to delete cache file: " + cacheFile); 246 } 247 } 248 249 final byte[] cacheEntry = toCacheEntry(parsed); 250 251 if (cacheEntry == null) { 252 return; 253 } 254 255 try (FileOutputStream fos = new FileOutputStream(cacheFile)) { 256 fos.write(cacheEntry); 257 } catch (IOException ioe) { 258 Slog.w(TAG, "Error writing cache entry.", ioe); 259 cacheFile.delete(); 260 } 261 } catch (Throwable e) { 262 Slog.w(TAG, "Error saving package cache.", e); 263 } 264 } 265 266 /** 267 * Delete the cache files for the given {@code packageFile}. 268 */ cleanCachedResult(@onNull File packageFile)269 public void cleanCachedResult(@NonNull File packageFile) { 270 final String packageName = packageFile.getName(); 271 final File[] files = FileUtils.listFilesOrEmpty(mCacheDir, 272 (dir, name) -> name.startsWith(packageName)); 273 for (File file : files) { 274 if (!file.delete()) { 275 Slog.e(TAG, "Unable to clean cache file: " + file); 276 } 277 } 278 } 279 } 280