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