• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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