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