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