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 File[] rollbackDirs = rollbackDataDir.listFiles(); 88 if (rollbackDirs == null) { 89 Slog.e(TAG, "Folder doesn't exist: " + rollbackDataDir); 90 return rollbacks; 91 } 92 for (File rollbackDir : rollbackDirs) { 93 if (rollbackDir.isDirectory()) { 94 try { 95 rollbacks.add(loadRollback(rollbackDir)); 96 } catch (IOException e) { 97 Slog.e(TAG, "Unable to read rollback at " + rollbackDir, e); 98 removeFile(rollbackDir); 99 } 100 } 101 } 102 return rollbacks; 103 } 104 loadRollbacks()105 List<Rollback> loadRollbacks() { 106 return loadRollbacks(mRollbackDataDir); 107 } 108 loadHistorialRollbacks()109 List<Rollback> loadHistorialRollbacks() { 110 return loadRollbacks(mRollbackHistoryDir); 111 } 112 113 /** 114 * Converts a {@code JSONArray} of integers to a {@code List<Integer>}. 115 */ toIntList(@onNull JSONArray jsonArray)116 private static @NonNull List<Integer> toIntList(@NonNull JSONArray jsonArray) 117 throws JSONException { 118 final List<Integer> ret = new ArrayList<>(); 119 for (int i = 0; i < jsonArray.length(); ++i) { 120 ret.add(jsonArray.getInt(i)); 121 } 122 123 return ret; 124 } 125 126 /** 127 * Converts a {@code List<Integer>} into a {@code JSONArray} of integers. 128 */ fromIntList(@onNull List<Integer> list)129 private static @NonNull JSONArray fromIntList(@NonNull List<Integer> list) { 130 JSONArray jsonArray = new JSONArray(); 131 for (int i = 0; i < list.size(); ++i) { 132 jsonArray.put(list.get(i)); 133 } 134 135 return jsonArray; 136 } 137 convertToJsonArray(@onNull List<RestoreInfo> list)138 private static @NonNull JSONArray convertToJsonArray(@NonNull List<RestoreInfo> list) 139 throws JSONException { 140 JSONArray jsonArray = new JSONArray(); 141 for (RestoreInfo ri : list) { 142 JSONObject jo = new JSONObject(); 143 jo.put("userId", ri.userId); 144 jo.put("appId", ri.appId); 145 jo.put("seInfo", ri.seInfo); 146 jsonArray.put(jo); 147 } 148 149 return jsonArray; 150 } 151 convertToRestoreInfoArray( @onNull JSONArray array)152 private static @NonNull ArrayList<RestoreInfo> convertToRestoreInfoArray( 153 @NonNull JSONArray array) throws JSONException { 154 ArrayList<RestoreInfo> restoreInfos = new ArrayList<>(); 155 156 for (int i = 0; i < array.length(); ++i) { 157 JSONObject jo = array.getJSONObject(i); 158 restoreInfos.add(new RestoreInfo( 159 jo.getInt("userId"), 160 jo.getInt("appId"), 161 jo.getString("seInfo"))); 162 } 163 164 return restoreInfos; 165 } 166 extensionVersionsToJson( SparseIntArray extensionVersions)167 private static @NonNull JSONArray extensionVersionsToJson( 168 SparseIntArray extensionVersions) throws JSONException { 169 JSONArray array = new JSONArray(); 170 for (int i = 0; i < extensionVersions.size(); i++) { 171 JSONObject entryJson = new JSONObject(); 172 entryJson.put("sdkVersion", extensionVersions.keyAt(i)); 173 entryJson.put("extensionVersion", extensionVersions.valueAt(i)); 174 array.put(entryJson); 175 } 176 return array; 177 } 178 extensionVersionsFromJson(JSONArray json)179 private static @NonNull SparseIntArray extensionVersionsFromJson(JSONArray json) 180 throws JSONException { 181 if (json == null) { 182 return new SparseIntArray(0); 183 } 184 SparseIntArray extensionVersions = new SparseIntArray(json.length()); 185 for (int i = 0; i < json.length(); i++) { 186 JSONObject entry = json.getJSONObject(i); 187 extensionVersions.append( 188 entry.getInt("sdkVersion"), entry.getInt("extensionVersion")); 189 } 190 return extensionVersions; 191 } 192 rollbackInfoToJson(RollbackInfo rollback)193 private static JSONObject rollbackInfoToJson(RollbackInfo rollback) throws JSONException { 194 JSONObject json = new JSONObject(); 195 json.put("rollbackId", rollback.getRollbackId()); 196 json.put("packages", toJson(rollback.getPackages())); 197 json.put("isStaged", rollback.isStaged()); 198 json.put("causePackages", versionedPackagesToJson(rollback.getCausePackages())); 199 json.put("committedSessionId", rollback.getCommittedSessionId()); 200 json.put("rollbackImpactLevel", rollback.getRollbackImpactLevel()); 201 return json; 202 } 203 rollbackInfoFromJson(JSONObject json)204 private static RollbackInfo rollbackInfoFromJson(JSONObject json) throws JSONException { 205 RollbackInfo rollbackInfo = new RollbackInfo( 206 json.getInt("rollbackId"), 207 packageRollbackInfosFromJson(json.getJSONArray("packages")), 208 json.getBoolean("isStaged"), 209 versionedPackagesFromJson(json.getJSONArray("causePackages")), 210 json.getInt("committedSessionId")); 211 212 // to make it backward compatible. 213 rollbackInfo.setRollbackImpactLevel(json.optInt("rollbackImpactLevel", 214 PackageManager.ROLLBACK_USER_IMPACT_LOW)); 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