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