1 /*
2  * Copyright (C) 2018 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.rollback;
18 
19 import static com.android.server.rollback.Rollback.rollbackStateFromString;
20 
21 import android.annotation.NonNull;
22 import android.content.pm.PackageManager;
23 import android.content.pm.VersionedPackage;
24 import android.content.rollback.PackageRollbackInfo;
25 import android.content.rollback.PackageRollbackInfo.RestoreInfo;
26 import android.content.rollback.RollbackInfo;
27 import android.os.SystemProperties;
28 import android.os.UserHandle;
29 import android.system.ErrnoException;
30 import android.system.Os;
31 import android.util.AtomicFile;
32 import android.util.Slog;
33 import android.util.SparseIntArray;
34 
35 import com.android.internal.annotations.VisibleForTesting;
36 
37 import libcore.io.IoUtils;
38 
39 import org.json.JSONArray;
40 import org.json.JSONException;
41 import org.json.JSONObject;
42 
43 import java.io.File;
44 import java.io.FileOutputStream;
45 import java.io.IOException;
46 import java.nio.file.Files;
47 import java.text.ParseException;
48 import java.time.Instant;
49 import java.time.format.DateTimeParseException;
50 import java.util.ArrayList;
51 import java.util.List;
52 
53 /**
54  * Helper class for loading and saving rollback data to persistent storage.
55  */
56 class RollbackStore {
57     private static final String TAG = "RollbackManager";
58 
59     // Assuming the rollback data directory is /data/rollback, we use the
60     // following directory structure to store persisted data for rollbacks:
61     //   /data/rollback/
62     //       XXX/
63     //           rollback.json
64     //           com.package.A/
65     //               base.apk
66     //           com.package.B/
67     //               base.apk
68     //       YYY/
69     //           rollback.json
70     //
71     // * XXX, YYY are the rollbackIds for the corresponding rollbacks.
72     // * rollback.json contains all relevant metadata for the rollback.
73     private final File mRollbackDataDir;
74     private final File mRollbackHistoryDir;
75 
RollbackStore(File rollbackDataDir, File rollbackHistoryDir)76     RollbackStore(File rollbackDataDir, File rollbackHistoryDir) {
77         mRollbackDataDir = rollbackDataDir;
78         mRollbackHistoryDir = rollbackHistoryDir;
79     }
80 
81     /**
82      * Reads the rollbacks from persistent storage.
83      */
loadRollbacks(File rollbackDataDir)84     private static List<Rollback> loadRollbacks(File rollbackDataDir) {
85         List<Rollback> rollbacks = new ArrayList<>();
86         rollbackDataDir.mkdirs();
87         for (File rollbackDir : rollbackDataDir.listFiles()) {
88             if (rollbackDir.isDirectory()) {
89                 try {
90                     rollbacks.add(loadRollback(rollbackDir));
91                 } catch (IOException e) {
92                     Slog.e(TAG, "Unable to read rollback at " + rollbackDir, e);
93                     removeFile(rollbackDir);
94                 }
95             }
96         }
97         return rollbacks;
98     }
99 
loadRollbacks()100     List<Rollback> loadRollbacks() {
101         return loadRollbacks(mRollbackDataDir);
102     }
103 
loadHistorialRollbacks()104     List<Rollback> loadHistorialRollbacks() {
105         return loadRollbacks(mRollbackHistoryDir);
106     }
107 
108     /**
109      * Converts a {@code JSONArray} of integers to a {@code List<Integer>}.
110      */
toIntList(@onNull JSONArray jsonArray)111     private static @NonNull List<Integer> toIntList(@NonNull JSONArray jsonArray)
112             throws JSONException {
113         final List<Integer> ret = new ArrayList<>();
114         for (int i = 0; i < jsonArray.length(); ++i) {
115             ret.add(jsonArray.getInt(i));
116         }
117 
118         return ret;
119     }
120 
121     /**
122      * Converts a {@code List<Integer>} into a {@code JSONArray} of integers.
123      */
fromIntList(@onNull List<Integer> list)124     private static @NonNull JSONArray fromIntList(@NonNull List<Integer> list) {
125         JSONArray jsonArray = new JSONArray();
126         for (int i = 0; i < list.size(); ++i) {
127             jsonArray.put(list.get(i));
128         }
129 
130         return jsonArray;
131     }
132 
convertToJsonArray(@onNull List<RestoreInfo> list)133     private static @NonNull JSONArray convertToJsonArray(@NonNull List<RestoreInfo> list)
134             throws JSONException {
135         JSONArray jsonArray = new JSONArray();
136         for (RestoreInfo ri : list) {
137             JSONObject jo = new JSONObject();
138             jo.put("userId", ri.userId);
139             jo.put("appId", ri.appId);
140             jo.put("seInfo", ri.seInfo);
141             jsonArray.put(jo);
142         }
143 
144         return jsonArray;
145     }
146 
convertToRestoreInfoArray( @onNull JSONArray array)147     private static @NonNull ArrayList<RestoreInfo> convertToRestoreInfoArray(
148             @NonNull JSONArray array) throws JSONException {
149         ArrayList<RestoreInfo> restoreInfos = new ArrayList<>();
150 
151         for (int i = 0; i < array.length(); ++i) {
152             JSONObject jo = array.getJSONObject(i);
153             restoreInfos.add(new RestoreInfo(
154                     jo.getInt("userId"),
155                     jo.getInt("appId"),
156                     jo.getString("seInfo")));
157         }
158 
159         return restoreInfos;
160     }
161 
extensionVersionsToJson( SparseIntArray extensionVersions)162     private static @NonNull JSONArray extensionVersionsToJson(
163             SparseIntArray extensionVersions) throws JSONException {
164         JSONArray array = new JSONArray();
165         for (int i = 0; i < extensionVersions.size(); i++) {
166             JSONObject entryJson = new JSONObject();
167             entryJson.put("sdkVersion", extensionVersions.keyAt(i));
168             entryJson.put("extensionVersion", extensionVersions.valueAt(i));
169             array.put(entryJson);
170         }
171         return array;
172     }
173 
extensionVersionsFromJson(JSONArray json)174     private static @NonNull SparseIntArray extensionVersionsFromJson(JSONArray json)
175             throws JSONException {
176         if (json == null) {
177             return new SparseIntArray(0);
178         }
179         SparseIntArray extensionVersions = new SparseIntArray(json.length());
180         for (int i = 0; i < json.length(); i++) {
181             JSONObject entry = json.getJSONObject(i);
182             extensionVersions.append(
183                     entry.getInt("sdkVersion"), entry.getInt("extensionVersion"));
184         }
185         return extensionVersions;
186     }
187 
rollbackInfoToJson(RollbackInfo rollback)188     private static JSONObject rollbackInfoToJson(RollbackInfo rollback) throws JSONException {
189         JSONObject json = new JSONObject();
190         json.put("rollbackId", rollback.getRollbackId());
191         json.put("packages", toJson(rollback.getPackages()));
192         json.put("isStaged", rollback.isStaged());
193         json.put("causePackages", versionedPackagesToJson(rollback.getCausePackages()));
194         json.put("committedSessionId", rollback.getCommittedSessionId());
195         return json;
196     }
197 
rollbackInfoFromJson(JSONObject json)198     private static RollbackInfo rollbackInfoFromJson(JSONObject json) throws JSONException {
199         return new RollbackInfo(
200                 json.getInt("rollbackId"),
201                 packageRollbackInfosFromJson(json.getJSONArray("packages")),
202                 json.getBoolean("isStaged"),
203                 versionedPackagesFromJson(json.getJSONArray("causePackages")),
204                 json.getInt("committedSessionId"));
205     }
206 
207     /**
208      * Creates a new Rollback instance for a non-staged rollback with
209      * backupDir assigned.
210      */
createNonStagedRollback(int rollbackId, int originalSessionId, int userId, String installerPackageName, int[] packageSessionIds, SparseIntArray extensionVersions)211     Rollback createNonStagedRollback(int rollbackId, int originalSessionId, int userId,
212             String installerPackageName, int[] packageSessionIds,
213             SparseIntArray extensionVersions) {
214         File backupDir = new File(mRollbackDataDir, Integer.toString(rollbackId));
215         return new Rollback(rollbackId, backupDir, originalSessionId, /* isStaged */ false, userId,
216                 installerPackageName, packageSessionIds, extensionVersions);
217     }
218 
219     /**
220      * Creates a new Rollback instance for a staged rollback with
221      * backupDir assigned.
222      */
createStagedRollback(int rollbackId, int originalSessionId, int userId, String installerPackageName, int[] packageSessionIds, SparseIntArray extensionVersions)223     Rollback createStagedRollback(int rollbackId, int originalSessionId, int userId,
224             String installerPackageName, int[] packageSessionIds,
225             SparseIntArray extensionVersions) {
226         File backupDir = new File(mRollbackDataDir, Integer.toString(rollbackId));
227         return new Rollback(rollbackId, backupDir, originalSessionId, /* isStaged */ true, userId,
228                 installerPackageName, packageSessionIds, extensionVersions);
229     }
230 
isLinkPossible(File oldFile, File newFile)231     private static boolean isLinkPossible(File oldFile, File newFile) {
232         try {
233             return Os.stat(oldFile.getAbsolutePath()).st_dev
234                     == Os.stat(newFile.getAbsolutePath()).st_dev;
235         } catch (ErrnoException ignore) {
236             return false;
237         }
238     }
239 
240     /**
241      * Creates a backup copy of an apk or apex for a package.
242      * For packages containing splits, this method should be called for each
243      * of the package's split apks in addition to the base apk.
244      */
backupPackageCodePath(Rollback rollback, String packageName, String codePath)245     static void backupPackageCodePath(Rollback rollback, String packageName, String codePath)
246             throws IOException {
247         File sourceFile = new File(codePath);
248         File targetDir = new File(rollback.getBackupDir(), packageName);
249         targetDir.mkdirs();
250         File targetFile = new File(targetDir, sourceFile.getName());
251 
252         boolean fallbackToCopy = !isLinkPossible(sourceFile, targetDir);
253         if (!fallbackToCopy) {
254             try {
255                 // Create a hard link to avoid copy
256                 // TODO(b/168562373)
257                 // Linking between non-encrypted and encrypted is not supported and we have
258                 // encrypted /data/rollback and non-encrypted /data/apex/active. For now this works
259                 // because we happen to store encrypted files under /data/apex/active which is no
260                 // longer the case when compressed apex rolls out. We have to handle this case in
261                 // order not to fall back to copy.
262                 Os.link(sourceFile.getAbsolutePath(), targetFile.getAbsolutePath());
263             } catch (ErrnoException e) {
264                 boolean isRollbackTest =
265                         SystemProperties.getBoolean("persist.rollback.is_test", false);
266                 if (isRollbackTest) {
267                     throw new IOException(e);
268                 } else {
269                     fallbackToCopy = true;
270                 }
271             }
272         }
273 
274         if (fallbackToCopy) {
275             // Fall back to copy if hardlink can't be created
276             Files.copy(sourceFile.toPath(), targetFile.toPath());
277         }
278     }
279 
280     /**
281      * Returns the apk or apex files backed up for the given package.
282      * Includes the base apk and any splits. Returns null if none found.
283      */
getPackageCodePaths(Rollback rollback, String packageName)284     static File[] getPackageCodePaths(Rollback rollback, String packageName) {
285         File targetDir = new File(rollback.getBackupDir(), packageName);
286         File[] files = targetDir.listFiles();
287         if (files == null || files.length == 0) {
288             return null;
289         }
290         return files;
291     }
292 
293     /**
294      * Deletes all backed up apks and apex files associated with the given
295      * rollback.
296      */
deletePackageCodePaths(Rollback rollback)297     static void deletePackageCodePaths(Rollback rollback) {
298         for (PackageRollbackInfo info : rollback.info.getPackages()) {
299             File targetDir = new File(rollback.getBackupDir(), info.getPackageName());
300             removeFile(targetDir);
301         }
302     }
303 
304     /**
305      * Saves the given rollback to persistent storage.
306      */
saveRollback(Rollback rollback, File backDir)307     private static void saveRollback(Rollback rollback, File backDir) {
308         FileOutputStream fos = null;
309         AtomicFile file = new AtomicFile(new File(backDir, "rollback.json"));
310         try {
311             backDir.mkdirs();
312             JSONObject dataJson = new JSONObject();
313             dataJson.put("info", rollbackInfoToJson(rollback.info));
314             dataJson.put("timestamp", rollback.getTimestamp().toString());
315             dataJson.put("originalSessionId", rollback.getOriginalSessionId());
316             dataJson.put("state", rollback.getStateAsString());
317             dataJson.put("stateDescription", rollback.getStateDescription());
318             dataJson.put("restoreUserDataInProgress", rollback.isRestoreUserDataInProgress());
319             dataJson.put("userId", rollback.getUserId());
320             dataJson.putOpt("installerPackageName", rollback.getInstallerPackageName());
321             dataJson.putOpt(
322                     "extensionVersions", extensionVersionsToJson(rollback.getExtensionVersions()));
323 
324             fos = file.startWrite();
325             fos.write(dataJson.toString().getBytes());
326             fos.flush();
327             file.finishWrite(fos);
328         } catch (JSONException | IOException e) {
329             Slog.e(TAG, "Unable to save rollback for: " + rollback.info.getRollbackId(), e);
330             if (fos != null) {
331                 file.failWrite(fos);
332             }
333         }
334     }
335 
saveRollback(Rollback rollback)336     static void saveRollback(Rollback rollback) {
337         saveRollback(rollback, rollback.getBackupDir());
338     }
339 
340     /**
341      * Saves the rollback to $mRollbackHistoryDir/ROLLBACKID-HEX for debugging purpose.
342      */
saveRollbackToHistory(Rollback rollback)343     void saveRollbackToHistory(Rollback rollback) {
344         // The same id might be allocated to different historical rollbacks.
345         // Let's add a suffix to avoid naming collision.
346         String suffix = Long.toHexString(rollback.getTimestamp().getEpochSecond());
347         String dirName = Integer.toString(rollback.info.getRollbackId());
348         File backupDir = new File(mRollbackHistoryDir, dirName + "-" + suffix);
349         saveRollback(rollback, backupDir);
350     }
351 
352     /**
353      * Removes all persistent storage associated with the given rollback.
354      */
deleteRollback(Rollback rollback)355     static void deleteRollback(Rollback rollback) {
356         removeFile(rollback.getBackupDir());
357     }
358 
359     /**
360      * Reads the metadata for a rollback from the given directory.
361      * @throws IOException in case of error reading the data.
362      */
loadRollback(File backupDir)363     private static Rollback loadRollback(File backupDir) throws IOException {
364         try {
365             File rollbackJsonFile = new File(backupDir, "rollback.json");
366             JSONObject dataJson = new JSONObject(
367                     IoUtils.readFileAsString(rollbackJsonFile.getAbsolutePath()));
368 
369             return rollbackFromJson(dataJson, backupDir);
370         } catch (JSONException | DateTimeParseException | ParseException e) {
371             throw new IOException(e);
372         }
373     }
374 
375     @VisibleForTesting
rollbackFromJson(JSONObject dataJson, File backupDir)376     static Rollback rollbackFromJson(JSONObject dataJson, File backupDir)
377             throws JSONException, ParseException {
378         return new Rollback(
379                 rollbackInfoFromJson(dataJson.getJSONObject("info")),
380                 backupDir,
381                 Instant.parse(dataJson.getString("timestamp")),
382                 // Backward compatibility: Historical rollbacks are not erased upon OTA update.
383                 //  Need to load the old field 'stagedSessionId' as fallback.
384                 dataJson.optInt("originalSessionId", dataJson.optInt("stagedSessionId", -1)),
385                 rollbackStateFromString(dataJson.getString("state")),
386                 dataJson.optString("stateDescription"),
387                 dataJson.getBoolean("restoreUserDataInProgress"),
388                 dataJson.optInt("userId", UserHandle.SYSTEM.getIdentifier()),
389                 dataJson.optString("installerPackageName", ""),
390                 extensionVersionsFromJson(dataJson.optJSONArray("extensionVersions")));
391     }
392 
toJson(VersionedPackage pkg)393     private static JSONObject toJson(VersionedPackage pkg) throws JSONException {
394         JSONObject json = new JSONObject();
395         json.put("packageName", pkg.getPackageName());
396         json.put("longVersionCode", pkg.getLongVersionCode());
397         return json;
398     }
399 
versionedPackageFromJson(JSONObject json)400     private static VersionedPackage versionedPackageFromJson(JSONObject json) throws JSONException {
401         String packageName = json.getString("packageName");
402         long longVersionCode = json.getLong("longVersionCode");
403         return new VersionedPackage(packageName, longVersionCode);
404     }
405 
toJson(PackageRollbackInfo info)406     private static JSONObject toJson(PackageRollbackInfo info) throws JSONException {
407         JSONObject json = new JSONObject();
408         json.put("versionRolledBackFrom", toJson(info.getVersionRolledBackFrom()));
409         json.put("versionRolledBackTo", toJson(info.getVersionRolledBackTo()));
410 
411         List<Integer> pendingBackups = info.getPendingBackups();
412         List<RestoreInfo> pendingRestores = info.getPendingRestores();
413         List<Integer> snapshottedUsers = info.getSnapshottedUsers();
414         json.put("pendingBackups", fromIntList(pendingBackups));
415         json.put("pendingRestores", convertToJsonArray(pendingRestores));
416 
417         json.put("isApex", info.isApex());
418         json.put("isApkInApex", info.isApkInApex());
419 
420         // Field is named 'installedUsers' for legacy reasons.
421         json.put("installedUsers", fromIntList(snapshottedUsers));
422 
423         json.put("rollbackDataPolicy", info.getRollbackDataPolicy());
424 
425         return json;
426     }
427 
packageRollbackInfoFromJson(JSONObject json)428     private static PackageRollbackInfo packageRollbackInfoFromJson(JSONObject json)
429             throws JSONException {
430         VersionedPackage versionRolledBackFrom = versionedPackageFromJson(
431                 json.getJSONObject("versionRolledBackFrom"));
432         VersionedPackage versionRolledBackTo = versionedPackageFromJson(
433                 json.getJSONObject("versionRolledBackTo"));
434 
435         final List<Integer> pendingBackups = toIntList(
436                 json.getJSONArray("pendingBackups"));
437         final ArrayList<RestoreInfo> pendingRestores = convertToRestoreInfoArray(
438                 json.getJSONArray("pendingRestores"));
439 
440         final boolean isApex = json.getBoolean("isApex");
441         final boolean isApkInApex = json.getBoolean("isApkInApex");
442 
443         // Field is named 'installedUsers' for legacy reasons.
444         final List<Integer> snapshottedUsers = toIntList(json.getJSONArray("installedUsers"));
445 
446         // Backward compatibility: no such field for old versions.
447         final int rollbackDataPolicy = json.optInt("rollbackDataPolicy",
448                 PackageManager.ROLLBACK_DATA_POLICY_RESTORE);
449 
450         return new PackageRollbackInfo(versionRolledBackFrom, versionRolledBackTo,
451                 pendingBackups, pendingRestores, isApex, isApkInApex, snapshottedUsers,
452                 rollbackDataPolicy);
453     }
454 
versionedPackagesToJson(List<VersionedPackage> packages)455     private static JSONArray versionedPackagesToJson(List<VersionedPackage> packages)
456             throws JSONException {
457         JSONArray json = new JSONArray();
458         for (VersionedPackage pkg : packages) {
459             json.put(toJson(pkg));
460         }
461         return json;
462     }
463 
versionedPackagesFromJson(JSONArray json)464     private static List<VersionedPackage> versionedPackagesFromJson(JSONArray json)
465             throws JSONException {
466         List<VersionedPackage> packages = new ArrayList<>();
467         for (int i = 0; i < json.length(); ++i) {
468             packages.add(versionedPackageFromJson(json.getJSONObject(i)));
469         }
470         return packages;
471     }
472 
toJson(List<PackageRollbackInfo> infos)473     private static JSONArray toJson(List<PackageRollbackInfo> infos) throws JSONException {
474         JSONArray json = new JSONArray();
475         for (PackageRollbackInfo info : infos) {
476             json.put(toJson(info));
477         }
478         return json;
479     }
480 
packageRollbackInfosFromJson(JSONArray json)481     private static List<PackageRollbackInfo> packageRollbackInfosFromJson(JSONArray json)
482             throws JSONException {
483         List<PackageRollbackInfo> infos = new ArrayList<>();
484         for (int i = 0; i < json.length(); ++i) {
485             infos.add(packageRollbackInfoFromJson(json.getJSONObject(i)));
486         }
487         return infos;
488     }
489 
490     /**
491      * Deletes a file completely.
492      * If the file is a directory, its contents are deleted as well.
493      * Has no effect if the directory does not exist.
494      */
removeFile(File file)495     private static void removeFile(File file) {
496         if (file.isDirectory()) {
497             for (File child : file.listFiles()) {
498                 removeFile(child);
499             }
500         }
501         if (file.exists()) {
502             file.delete();
503         }
504     }
505 }
506