1 /* 2 * Copyright (C) 2019 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 android.annotation.AnyThread; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.annotation.WorkerThread; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.pm.ApplicationInfo; 26 import android.content.pm.PackageManager; 27 import android.content.pm.VersionedPackage; 28 import android.content.rollback.PackageRollbackInfo; 29 import android.content.rollback.RollbackInfo; 30 import android.content.rollback.RollbackManager; 31 import android.os.Environment; 32 import android.os.FileUtils; 33 import android.os.Handler; 34 import android.os.HandlerThread; 35 import android.os.PowerManager; 36 import android.os.SystemProperties; 37 import android.util.ArraySet; 38 import android.util.Slog; 39 import android.util.SparseArray; 40 41 import com.android.internal.annotations.VisibleForTesting; 42 import com.android.internal.util.FrameworkStatsLog; 43 import com.android.internal.util.Preconditions; 44 import com.android.server.PackageWatchdog; 45 import com.android.server.PackageWatchdog.FailureReasons; 46 import com.android.server.PackageWatchdog.PackageHealthObserver; 47 import com.android.server.PackageWatchdog.PackageHealthObserverImpact; 48 import com.android.server.SystemConfig; 49 import com.android.server.pm.ApexManager; 50 51 import java.io.BufferedReader; 52 import java.io.File; 53 import java.io.FileInputStream; 54 import java.io.FileOutputStream; 55 import java.io.FileReader; 56 import java.io.IOException; 57 import java.io.PrintWriter; 58 import java.util.Collections; 59 import java.util.List; 60 import java.util.Set; 61 import java.util.function.Consumer; 62 63 /** 64 * {@link PackageHealthObserver} for {@link RollbackManagerService}. 65 * This class monitors crashes and triggers RollbackManager rollback accordingly. 66 * It also monitors native crashes for some short while after boot. 67 * 68 * @hide 69 */ 70 final class RollbackPackageHealthObserver implements PackageHealthObserver { 71 private static final String TAG = "RollbackPackageHealthObserver"; 72 private static final String NAME = "rollback-observer"; 73 private static final String PROP_ATTEMPTING_REBOOT = "sys.attempting_reboot"; 74 private static final int PERSISTENT_MASK = ApplicationInfo.FLAG_PERSISTENT 75 | ApplicationInfo.FLAG_SYSTEM; 76 77 private final Context mContext; 78 private final Handler mHandler; 79 private final ApexManager mApexManager; 80 private final File mLastStagedRollbackIdsFile; 81 private final File mTwoPhaseRollbackEnabledFile; 82 // Staged rollback ids that have been committed but their session is not yet ready 83 private final Set<Integer> mPendingStagedRollbackIds = new ArraySet<>(); 84 // True if needing to roll back only rebootless apexes when native crash happens 85 private boolean mTwoPhaseRollbackEnabled; 86 RollbackPackageHealthObserver(Context context)87 RollbackPackageHealthObserver(Context context) { 88 mContext = context; 89 HandlerThread handlerThread = new HandlerThread("RollbackPackageHealthObserver"); 90 handlerThread.start(); 91 mHandler = new Handler(handlerThread.getLooper()); 92 File dataDir = new File(Environment.getDataDirectory(), "rollback-observer"); 93 dataDir.mkdirs(); 94 mLastStagedRollbackIdsFile = new File(dataDir, "last-staged-rollback-ids"); 95 mTwoPhaseRollbackEnabledFile = new File(dataDir, "two-phase-rollback-enabled"); 96 PackageWatchdog.getInstance(mContext).registerHealthObserver(this); 97 mApexManager = ApexManager.getInstance(); 98 99 if (SystemProperties.getBoolean("sys.boot_completed", false)) { 100 // Load the value from the file if system server has crashed and restarted 101 mTwoPhaseRollbackEnabled = readBoolean(mTwoPhaseRollbackEnabledFile); 102 } else { 103 // Disable two-phase rollback for a normal reboot. We assume the rebootless apex 104 // installed before reboot is stable if native crash didn't happen. 105 mTwoPhaseRollbackEnabled = false; 106 writeBoolean(mTwoPhaseRollbackEnabledFile, false); 107 } 108 } 109 110 @Override onHealthCheckFailed(@ullable VersionedPackage failedPackage, @FailureReasons int failureReason, int mitigationCount)111 public int onHealthCheckFailed(@Nullable VersionedPackage failedPackage, 112 @FailureReasons int failureReason, int mitigationCount) { 113 boolean anyRollbackAvailable = !mContext.getSystemService(RollbackManager.class) 114 .getAvailableRollbacks().isEmpty(); 115 int impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; 116 117 if (failureReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH 118 && anyRollbackAvailable) { 119 // For native crashes, we will directly roll back any available rollbacks 120 // Note: For non-native crashes the rollback-all step has higher impact 121 impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_30; 122 } else if (getAvailableRollback(failedPackage) != null) { 123 // Rollback is available, we may get a callback into #execute 124 impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_30; 125 } else if (anyRollbackAvailable) { 126 // If any rollbacks are available, we will commit them 127 impact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_70; 128 } 129 130 return impact; 131 } 132 133 @Override execute(@ullable VersionedPackage failedPackage, @FailureReasons int rollbackReason, int mitigationCount)134 public boolean execute(@Nullable VersionedPackage failedPackage, 135 @FailureReasons int rollbackReason, int mitigationCount) { 136 if (rollbackReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH) { 137 mHandler.post(() -> rollbackAll(rollbackReason)); 138 return true; 139 } 140 141 RollbackInfo rollback = getAvailableRollback(failedPackage); 142 if (rollback != null) { 143 mHandler.post(() -> rollbackPackage(rollback, failedPackage, rollbackReason)); 144 } else { 145 mHandler.post(() -> rollbackAll(rollbackReason)); 146 } 147 148 // Assume rollbacks executed successfully 149 return true; 150 } 151 152 @Override getName()153 public String getName() { 154 return NAME; 155 } 156 157 @Override isPersistent()158 public boolean isPersistent() { 159 return true; 160 } 161 162 @Override mayObservePackage(String packageName)163 public boolean mayObservePackage(String packageName) { 164 if (mContext.getSystemService(RollbackManager.class) 165 .getAvailableRollbacks().isEmpty()) { 166 return false; 167 } 168 return isPersistentSystemApp(packageName); 169 } 170 isPersistentSystemApp(@onNull String packageName)171 private boolean isPersistentSystemApp(@NonNull String packageName) { 172 PackageManager pm = mContext.getPackageManager(); 173 try { 174 ApplicationInfo info = pm.getApplicationInfo(packageName, 0); 175 return (info.flags & PERSISTENT_MASK) == PERSISTENT_MASK; 176 } catch (PackageManager.NameNotFoundException e) { 177 return false; 178 } 179 } 180 assertInWorkerThread()181 private void assertInWorkerThread() { 182 Preconditions.checkState(mHandler.getLooper().isCurrentThread()); 183 } 184 185 /** 186 * Start observing health of {@code packages} for {@code durationMs}. 187 * This may cause {@code packages} to be rolled back if they crash too freqeuntly. 188 */ 189 @AnyThread startObservingHealth(List<String> packages, long durationMs)190 void startObservingHealth(List<String> packages, long durationMs) { 191 PackageWatchdog.getInstance(mContext).startObservingHealth(this, packages, durationMs); 192 } 193 194 @AnyThread notifyRollbackAvailable(RollbackInfo rollback)195 void notifyRollbackAvailable(RollbackInfo rollback) { 196 mHandler.post(() -> { 197 // Enable two-phase rollback when a rebootless apex rollback is made available. 198 // We assume the rebootless apex is stable and is less likely to be the cause 199 // if native crash doesn't happen before reboot. So we will clear the flag and disable 200 // two-phase rollback after reboot. 201 if (isRebootlessApex(rollback)) { 202 mTwoPhaseRollbackEnabled = true; 203 writeBoolean(mTwoPhaseRollbackEnabledFile, true); 204 } 205 }); 206 } 207 isRebootlessApex(RollbackInfo rollback)208 private static boolean isRebootlessApex(RollbackInfo rollback) { 209 if (!rollback.isStaged()) { 210 for (PackageRollbackInfo info : rollback.getPackages()) { 211 if (info.isApex()) { 212 return true; 213 } 214 } 215 } 216 return false; 217 } 218 219 /** Verifies the rollback state after a reboot and schedules polling for sometime after reboot 220 * to check for native crashes and mitigate them if needed. 221 */ 222 @AnyThread onBootCompletedAsync()223 void onBootCompletedAsync() { 224 mHandler.post(()->onBootCompleted()); 225 } 226 227 @WorkerThread onBootCompleted()228 private void onBootCompleted() { 229 assertInWorkerThread(); 230 231 RollbackManager rollbackManager = mContext.getSystemService(RollbackManager.class); 232 if (!rollbackManager.getAvailableRollbacks().isEmpty()) { 233 // TODO(gavincorkery): Call into Package Watchdog from outside the observer 234 PackageWatchdog.getInstance(mContext).scheduleCheckAndMitigateNativeCrashes(); 235 } 236 237 SparseArray<String> rollbackIds = popLastStagedRollbackIds(); 238 for (int i = 0; i < rollbackIds.size(); i++) { 239 WatchdogRollbackLogger.logRollbackStatusOnBoot(mContext, 240 rollbackIds.keyAt(i), rollbackIds.valueAt(i), 241 rollbackManager.getRecentlyCommittedRollbacks()); 242 } 243 } 244 245 @AnyThread getAvailableRollback(VersionedPackage failedPackage)246 private RollbackInfo getAvailableRollback(VersionedPackage failedPackage) { 247 RollbackManager rollbackManager = mContext.getSystemService(RollbackManager.class); 248 for (RollbackInfo rollback : rollbackManager.getAvailableRollbacks()) { 249 for (PackageRollbackInfo packageRollback : rollback.getPackages()) { 250 if (packageRollback.getVersionRolledBackFrom().equals(failedPackage)) { 251 return rollback; 252 } 253 // TODO(b/147666157): Extract version number of apk-in-apex so that we don't have 254 // to rely on complicated reasoning as below 255 256 // Due to b/147666157, for apk in apex, we do not know the version we are rolling 257 // back from. But if a package X is embedded in apex A exclusively (not embedded in 258 // any other apex), which is not guaranteed, then it is sufficient to check only 259 // package names here, as the version of failedPackage and the PackageRollbackInfo 260 // can't be different. If failedPackage has a higher version, then it must have 261 // been updated somehow. There are two ways: it was updated by an update of apex A 262 // or updated directly as apk. In both cases, this rollback would have gotten 263 // expired when onPackageReplaced() was called. Since the rollback exists, it has 264 // same version as failedPackage. 265 if (packageRollback.isApkInApex() 266 && packageRollback.getVersionRolledBackFrom().getPackageName() 267 .equals(failedPackage.getPackageName())) { 268 return rollback; 269 } 270 } 271 } 272 return null; 273 } 274 275 /** 276 * Returns {@code true} if staged session associated with {@code rollbackId} was marked 277 * as handled, {@code false} if already handled. 278 */ 279 @WorkerThread markStagedSessionHandled(int rollbackId)280 private boolean markStagedSessionHandled(int rollbackId) { 281 assertInWorkerThread(); 282 return mPendingStagedRollbackIds.remove(rollbackId); 283 } 284 285 /** 286 * Returns {@code true} if all pending staged rollback sessions were marked as handled, 287 * {@code false} if there is any left. 288 */ 289 @WorkerThread isPendingStagedSessionsEmpty()290 private boolean isPendingStagedSessionsEmpty() { 291 assertInWorkerThread(); 292 return mPendingStagedRollbackIds.isEmpty(); 293 } 294 readBoolean(File file)295 private static boolean readBoolean(File file) { 296 try (FileInputStream fis = new FileInputStream(file)) { 297 return fis.read() == 1; 298 } catch (IOException ignore) { 299 return false; 300 } 301 } 302 writeBoolean(File file, boolean value)303 private static void writeBoolean(File file, boolean value) { 304 try (FileOutputStream fos = new FileOutputStream(file)) { 305 fos.write(value ? 1 : 0); 306 fos.flush(); 307 FileUtils.sync(fos); 308 } catch (IOException ignore) { 309 } 310 } 311 312 @WorkerThread saveStagedRollbackId(int stagedRollbackId, @Nullable VersionedPackage logPackage)313 private void saveStagedRollbackId(int stagedRollbackId, @Nullable VersionedPackage logPackage) { 314 assertInWorkerThread(); 315 writeStagedRollbackId(mLastStagedRollbackIdsFile, stagedRollbackId, logPackage); 316 } 317 writeStagedRollbackId(File file, int stagedRollbackId, @Nullable VersionedPackage logPackage)318 static void writeStagedRollbackId(File file, int stagedRollbackId, 319 @Nullable VersionedPackage logPackage) { 320 try { 321 FileOutputStream fos = new FileOutputStream(file, true); 322 PrintWriter pw = new PrintWriter(fos); 323 String logPackageName = logPackage != null ? logPackage.getPackageName() : ""; 324 pw.append(String.valueOf(stagedRollbackId)).append(",").append(logPackageName); 325 pw.println(); 326 pw.flush(); 327 FileUtils.sync(fos); 328 pw.close(); 329 } catch (IOException e) { 330 Slog.e(TAG, "Failed to save last staged rollback id", e); 331 file.delete(); 332 } 333 } 334 335 @WorkerThread popLastStagedRollbackIds()336 private SparseArray<String> popLastStagedRollbackIds() { 337 assertInWorkerThread(); 338 try { 339 return readStagedRollbackIds(mLastStagedRollbackIdsFile); 340 } finally { 341 mLastStagedRollbackIdsFile.delete(); 342 } 343 } 344 readStagedRollbackIds(File file)345 static SparseArray<String> readStagedRollbackIds(File file) { 346 SparseArray<String> result = new SparseArray<>(); 347 try { 348 String line; 349 BufferedReader reader = new BufferedReader(new FileReader(file)); 350 while ((line = reader.readLine()) != null) { 351 // Each line is of the format: "id,logging_package" 352 String[] values = line.trim().split(","); 353 String rollbackId = values[0]; 354 String logPackageName = ""; 355 if (values.length > 1) { 356 logPackageName = values[1]; 357 } 358 result.put(Integer.parseInt(rollbackId), logPackageName); 359 } 360 } catch (Exception ignore) { 361 return new SparseArray<>(); 362 } 363 return result; 364 } 365 366 367 /** 368 * Returns true if the package name is the name of a module. 369 */ 370 @AnyThread isModule(String packageName)371 private boolean isModule(String packageName) { 372 // Check if the package is an APK inside an APEX. If it is, use the parent APEX package when 373 // querying PackageManager. 374 String apexPackageName = mApexManager.getActiveApexPackageNameContainingPackage( 375 packageName); 376 if (apexPackageName != null) { 377 packageName = apexPackageName; 378 } 379 380 PackageManager pm = mContext.getPackageManager(); 381 try { 382 return pm.getModuleInfo(packageName, 0) != null; 383 } catch (PackageManager.NameNotFoundException ignore) { 384 return false; 385 } 386 } 387 388 /** 389 * Rolls back the session that owns {@code failedPackage} 390 * 391 * @param rollback {@code rollbackInfo} of the {@code failedPackage} 392 * @param failedPackage the package that needs to be rolled back 393 */ 394 @WorkerThread rollbackPackage(RollbackInfo rollback, VersionedPackage failedPackage, @FailureReasons int rollbackReason)395 private void rollbackPackage(RollbackInfo rollback, VersionedPackage failedPackage, 396 @FailureReasons int rollbackReason) { 397 assertInWorkerThread(); 398 399 if (isAutomaticRollbackDenied(SystemConfig.getInstance(), failedPackage)) { 400 Slog.d(TAG, "Automatic rollback not allowed for package " 401 + failedPackage.getPackageName()); 402 return; 403 } 404 405 final RollbackManager rollbackManager = mContext.getSystemService(RollbackManager.class); 406 int reasonToLog = WatchdogRollbackLogger.mapFailureReasonToMetric(rollbackReason); 407 final String failedPackageToLog; 408 if (rollbackReason == PackageWatchdog.FAILURE_REASON_NATIVE_CRASH) { 409 failedPackageToLog = SystemProperties.get( 410 "sys.init.updatable_crashing_process_name", ""); 411 } else { 412 failedPackageToLog = failedPackage.getPackageName(); 413 } 414 VersionedPackage logPackageTemp = null; 415 if (isModule(failedPackage.getPackageName())) { 416 logPackageTemp = WatchdogRollbackLogger.getLogPackage(mContext, failedPackage); 417 } 418 419 final VersionedPackage logPackage = logPackageTemp; 420 WatchdogRollbackLogger.logEvent(logPackage, 421 FrameworkStatsLog.WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_INITIATE, 422 reasonToLog, failedPackageToLog); 423 424 Consumer<Intent> onResult = result -> { 425 assertInWorkerThread(); 426 int status = result.getIntExtra(RollbackManager.EXTRA_STATUS, 427 RollbackManager.STATUS_FAILURE); 428 if (status == RollbackManager.STATUS_SUCCESS) { 429 if (rollback.isStaged()) { 430 int rollbackId = rollback.getRollbackId(); 431 saveStagedRollbackId(rollbackId, logPackage); 432 WatchdogRollbackLogger.logEvent(logPackage, 433 FrameworkStatsLog 434 .WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_BOOT_TRIGGERED, 435 reasonToLog, failedPackageToLog); 436 437 } else { 438 WatchdogRollbackLogger.logEvent(logPackage, 439 FrameworkStatsLog 440 .WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_SUCCESS, 441 reasonToLog, failedPackageToLog); 442 } 443 } else { 444 WatchdogRollbackLogger.logEvent(logPackage, 445 FrameworkStatsLog 446 .WATCHDOG_ROLLBACK_OCCURRED__ROLLBACK_TYPE__ROLLBACK_FAILURE, 447 reasonToLog, failedPackageToLog); 448 } 449 if (rollback.isStaged()) { 450 markStagedSessionHandled(rollback.getRollbackId()); 451 // Wait for all pending staged sessions to get handled before rebooting. 452 if (isPendingStagedSessionsEmpty()) { 453 SystemProperties.set(PROP_ATTEMPTING_REBOOT, "true"); 454 mContext.getSystemService(PowerManager.class).reboot("Rollback staged install"); 455 } 456 } 457 }; 458 459 final LocalIntentReceiver rollbackReceiver = new LocalIntentReceiver(result -> { 460 mHandler.post(() -> onResult.accept(result)); 461 }); 462 463 rollbackManager.commitRollback(rollback.getRollbackId(), 464 Collections.singletonList(failedPackage), rollbackReceiver.getIntentSender()); 465 } 466 467 /** 468 * Returns true if this package is not eligible for automatic rollback. 469 */ 470 @VisibleForTesting 471 @AnyThread isAutomaticRollbackDenied(SystemConfig systemConfig, VersionedPackage versionedPackage)472 public static boolean isAutomaticRollbackDenied(SystemConfig systemConfig, 473 VersionedPackage versionedPackage) { 474 return systemConfig.getAutomaticRollbackDenylistedPackages() 475 .contains(versionedPackage.getPackageName()); 476 } 477 478 /** 479 * Two-phase rollback: 480 * 1. roll back rebootless apexes first 481 * 2. roll back all remaining rollbacks if native crash doesn't stop after (1) is done 482 * 483 * This approach gives us a better chance to correctly attribute native crash to rebootless 484 * apex update without rolling back Mainline updates which might contains critical security 485 * fixes. 486 */ 487 @WorkerThread useTwoPhaseRollback(List<RollbackInfo> rollbacks)488 private boolean useTwoPhaseRollback(List<RollbackInfo> rollbacks) { 489 assertInWorkerThread(); 490 if (!mTwoPhaseRollbackEnabled) { 491 return false; 492 } 493 494 Slog.i(TAG, "Rolling back all rebootless APEX rollbacks"); 495 boolean found = false; 496 for (RollbackInfo rollback : rollbacks) { 497 if (isRebootlessApex(rollback)) { 498 VersionedPackage sample = rollback.getPackages().get(0).getVersionRolledBackFrom(); 499 rollbackPackage(rollback, sample, PackageWatchdog.FAILURE_REASON_NATIVE_CRASH); 500 found = true; 501 } 502 } 503 return found; 504 } 505 506 @WorkerThread rollbackAll(@ailureReasons int rollbackReason)507 private void rollbackAll(@FailureReasons int rollbackReason) { 508 assertInWorkerThread(); 509 RollbackManager rollbackManager = mContext.getSystemService(RollbackManager.class); 510 List<RollbackInfo> rollbacks = rollbackManager.getAvailableRollbacks(); 511 if (useTwoPhaseRollback(rollbacks)) { 512 return; 513 } 514 515 Slog.i(TAG, "Rolling back all available rollbacks"); 516 // Add all rollback ids to mPendingStagedRollbackIds, so that we do not reboot before all 517 // pending staged rollbacks are handled. 518 for (RollbackInfo rollback : rollbacks) { 519 if (rollback.isStaged()) { 520 mPendingStagedRollbackIds.add(rollback.getRollbackId()); 521 } 522 } 523 524 for (RollbackInfo rollback : rollbacks) { 525 VersionedPackage sample = rollback.getPackages().get(0).getVersionRolledBackFrom(); 526 rollbackPackage(rollback, sample, rollbackReason); 527 } 528 } 529 } 530