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