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