Extract verication code to a new class (2/n) * A new class (PackageSessionVerifier) to run pre-reboot verification for staged-sessions. * StagingManager.commitSession() is called after pre-reboot verification is done which will then notify the observers for staged apex. * Dead code will be removed in next CLs. Bug: 161121612 Test: atest StagingManagerTest Test: atest CtsAtomicInstallTestCases Test: atest CtsPackageInstallTestCases Test: atest CtsStagedInstallHostTestCases Change-Id: I9c80c34543af1d7f363b12af4a3720d3e6d4e7aa
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index 2595bb4..a5a7c8a 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -174,6 +174,7 @@ private final File mSessionsDir; private final InternalCallback mInternalCallback = new InternalCallback(); + private final PackageSessionVerifier mSessionVerifier; /** * Used for generating session IDs. Since this is created at boot time, @@ -259,6 +260,8 @@ mApexManager = ApexManager.getInstance(); mStagingManager = new StagingManager(context, apexParserSupplier, mInstallThread.getLooper()); + mSessionVerifier = new PackageSessionVerifier(context, mPm, mApexManager, + apexParserSupplier, mInstallThread.getLooper()); LocalServices.getService(SystemServiceManager.class).startService( new Lifecycle(context, this)); @@ -1160,6 +1163,11 @@ } @Override + public PackageSessionVerifier getSessionVerifier() { + return mSessionVerifier; + } + + @Override public void bypassNextStagedInstallerCheck(boolean value) { if (!isCalledBySystemOrShell(Binder.getCallingUid())) { throw new SecurityException("Caller not allowed to bypass staged installer check");
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index 709d359..5c1a5b8 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -2692,7 +2692,12 @@ // staged session or not. For a staged session, we will hand it over to the staging // manager to complete the installation. if (isStaged()) { - mStagingManager.commitSession(mStagedSession); + mSessionProvider.getSessionVerifier().verifyStaged(mStagedSession, (error, msg) -> { + mStagedSession.notifyEndPreRebootVerification(); + if (error == SessionInfo.STAGED_SESSION_NO_ERROR) { + mStagingManager.commitSession(mStagedSession); + } + }); return; }
diff --git a/services/core/java/com/android/server/pm/PackageSessionProvider.java b/services/core/java/com/android/server/pm/PackageSessionProvider.java index af11e77..ad5cf13 100644 --- a/services/core/java/com/android/server/pm/PackageSessionProvider.java +++ b/services/core/java/com/android/server/pm/PackageSessionProvider.java
@@ -16,7 +16,10 @@ package com.android.server.pm; -/** Provides access to individual sessions managed by the install service */ +/** + * Provides access to individual sessions managed by the install service as well as utilities + * used by the install process. + */ public interface PackageSessionProvider { /** @@ -25,4 +28,5 @@ */ PackageInstallerSession getSession(int sessionId); + PackageSessionVerifier getSessionVerifier(); }
diff --git a/services/core/java/com/android/server/pm/PackageSessionVerifier.java b/services/core/java/com/android/server/pm/PackageSessionVerifier.java new file mode 100644 index 0000000..3f5410d --- /dev/null +++ b/services/core/java/com/android/server/pm/PackageSessionVerifier.java
@@ -0,0 +1,485 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.pm; + +import android.apex.ApexInfo; +import android.apex.ApexInfoList; +import android.apex.ApexSessionInfo; +import android.apex.ApexSessionParams; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageInstaller.SessionInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManagerInternal; +import android.content.pm.SigningDetails; +import android.content.pm.parsing.PackageInfoWithoutStateUtils; +import android.content.pm.parsing.result.ParseResult; +import android.content.pm.parsing.result.ParseTypeImpl; +import android.content.rollback.RollbackInfo; +import android.content.rollback.RollbackManager; +import android.os.Handler; +import android.os.Looper; +import android.os.RemoteException; +import android.util.IntArray; +import android.util.Slog; +import android.util.apk.ApkSignatureVerifier; + +import com.android.internal.content.PackageHelper; +import com.android.server.LocalServices; +import com.android.server.pm.parsing.PackageParser2; +import com.android.server.pm.parsing.pkg.ParsedPackage; +import com.android.server.rollback.RollbackManagerInternal; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +final class PackageSessionVerifier { + private static final String TAG = "PackageSessionVerifier"; + + interface Callback { + void onResult(int returnCode, String msg); + } + + private final Context mContext; + private final PackageManagerService mPm; + private final ApexManager mApexManager; + private final Supplier<PackageParser2> mPackageParserSupplier; + // The handler thread to run verification tasks. Since all tasks run in the same thread, + // there is no need for synchronization. + private final Handler mHandler; + // Parent sessions for checking session conflicts. + private final List<StagingManager.StagedSession> mStagedSessions = new ArrayList<>(); + + PackageSessionVerifier(Context context, PackageManagerService pm, + ApexManager apexManager, Supplier<PackageParser2> packageParserSupplier, + Looper looper) { + mContext = context; + mPm = pm; + mApexManager = apexManager; + mPackageParserSupplier = packageParserSupplier; + mHandler = new Handler(looper); + } + + /** + * Starts pre-reboot verification for the staged-session. This operation is broken into the + * following phases: + * <ul> + * <li>Checks if multiple active sessions are supported.</li> + * <li>Checks conflicts in rollbacks and overlapping packages.</li> + * <li>Submits apex sessions to apex service.</li> + * <li>Validates signatures of apex files.</li> + * <li>Notifies the result of verification.</li> + * </ul> + * + * Note it is the responsibility of the caller to ensure the staging files remain unchanged + * while the verification is in progress. + */ + void verifyStaged(StagingManager.StagedSession session, Callback callback) { + Slog.d(TAG, "Starting preRebootVerification for session " + session.sessionId()); + mHandler.post(() -> { + try { + storeSession(session); + checkActiveSessions(); + checkRollbacks(session); + if (session.isMultiPackage()) { + for (StagingManager.StagedSession child : session.getChildSessions()) { + checkOverlaps(session, child); + } + } else { + checkOverlaps(session, session); + } + dispatchVerifyApex(session, callback); + } catch (PackageManagerException e) { + onVerificationFailure(session, callback, e.error, e.getMessage()); + } + }); + } + + /** + * Stores staged-sessions for checking package overlapping and rollback conflicts. + */ + private void storeSession(StagingManager.StagedSession session) { + mStagedSessions.add(session); + } + + private void onVerificationSuccess(StagingManager.StagedSession session, Callback callback) { + callback.onResult(SessionInfo.STAGED_SESSION_NO_ERROR, null); + } + + private void onVerificationFailure(StagingManager.StagedSession session, Callback callback, + @SessionInfo.StagedSessionErrorCode int errorCode, String errorMessage) { + if (!ensureActiveApexSessionIsAborted(session)) { + Slog.e(TAG, "Failed to abort apex session " + session.sessionId()); + // Safe to ignore active apex session abortion failure since session will be marked + // failed on next step and staging directory for session will be deleted. + } + session.setSessionFailed(errorCode, errorMessage); + callback.onResult(errorCode, errorMessage); + } + + private void dispatchVerifyApex(StagingManager.StagedSession session, Callback callback) { + mHandler.post(() -> { + try { + verifyApex(session); + dispatchEndVerification(session, callback); + } catch (PackageManagerException e) { + onVerificationFailure(session, callback, e.error, e.getMessage()); + } + }); + } + + private void dispatchEndVerification(StagingManager.StagedSession session, Callback callback) { + mHandler.post(() -> { + try { + endVerification(session); + onVerificationSuccess(session, callback); + } catch (PackageManagerException e) { + onVerificationFailure(session, callback, e.error, e.getMessage()); + } + }); + } + + /** + * Pre-reboot verification phase for apex files: + * + * <p><ul> + * <li>submits session to apex service</li> + * <li>validates signatures of apex files</li> + * </ul></p> + */ + private void verifyApex(StagingManager.StagedSession session) throws PackageManagerException { + int rollbackId = -1; + if ((session.sessionParams().installFlags & PackageManager.INSTALL_ENABLE_ROLLBACK) != 0) { + // If rollback is enabled for this session, we call through to the RollbackManager + // with the list of sessions it must enable rollback for. Note that + // notifyStagedSession is a synchronous operation. + final RollbackManagerInternal rm = + LocalServices.getService(RollbackManagerInternal.class); + try { + // NOTE: To stay consistent with the non-staged install flow, we don't fail the + // entire install if rollbacks can't be enabled. + rollbackId = rm.notifyStagedSession(session.sessionId()); + } catch (RuntimeException re) { + Slog.e(TAG, "Failed to notifyStagedSession for session: " + + session.sessionId(), re); + } + } else if (isRollback(session)) { + rollbackId = retrieveRollbackIdForCommitSession(session.sessionId()); + } + + final boolean hasApex = session.containsApexSession(); + // APEX checks. For single-package sessions, check if they contain an APEX. For + // multi-package sessions, find all the child sessions that contain an APEX. + if (hasApex) { + final List<PackageInfo> apexPackages = submitSessionToApexService(session, rollbackId); + for (int i = 0, size = apexPackages.size(); i < size; i++) { + validateApexSignature(apexPackages.get(i)); + } + final PackageManagerInternal packageManagerInternal = + LocalServices.getService(PackageManagerInternal.class); + packageManagerInternal.pruneCachedApksInApex(apexPackages); + } + } + + /** + * Pre-reboot verification phase for wrapping up: + * <p><ul> + * <li>enables checkpoint if supported</li> + * <li>marks session as ready</li> + * </ul></p> + */ + private void endVerification(StagingManager.StagedSession session) + throws PackageManagerException { + // Before marking the session as ready, start checkpoint service if available + try { + if (PackageHelper.getStorageManager().supportsCheckpoint()) { + PackageHelper.getStorageManager().startCheckpoint(2); + } + } catch (Exception e) { + // Failed to get hold of StorageManager + Slog.e(TAG, "Failed to get hold of StorageManager", e); + throw new PackageManagerException( + SessionInfo.STAGED_SESSION_UNKNOWN, + "Failed to get hold of StorageManager"); + } + // Proactively mark session as ready before calling apexd. Although this call order + // looks counter-intuitive, this is the easiest way to ensure that session won't end up + // in the inconsistent state: + // - If device gets rebooted right before call to apexd, then apexd will never activate + // apex files of this staged session. This will result in StagingManager failing + // the session. + // On the other hand, if the order of the calls was inverted (first call apexd, then + // mark session as ready), then if a device gets rebooted right after the call to apexd, + // only apex part of the train will be applied, leaving device in an inconsistent state. + Slog.d(TAG, "Marking session " + session.sessionId() + " as ready"); + session.setSessionReady(); + if (session.isSessionReady()) { + final boolean hasApex = session.containsApexSession(); + if (hasApex) { + mApexManager.markStagedSessionReady(session.sessionId()); + } + } + } + + /** + * Validates the signature used to sign the container of the new apex package + * + * @param newApexPkg The new apex package that is being installed + */ + private void validateApexSignature(PackageInfo newApexPkg) throws PackageManagerException { + // Get signing details of the new package + final String apexPath = newApexPkg.applicationInfo.sourceDir; + final String packageName = newApexPkg.packageName; + int minSignatureScheme = ApkSignatureVerifier.getMinimumSignatureSchemeVersionForTargetSdk( + newApexPkg.applicationInfo.targetSdkVersion); + + final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing(); + final ParseResult<SigningDetails> newResult = ApkSignatureVerifier.verify( + input.reset(), apexPath, minSignatureScheme); + if (newResult.isError()) { + throw new PackageManagerException(SessionInfo.STAGED_SESSION_VERIFICATION_FAILED, + "Failed to parse APEX package " + apexPath + " : " + + newResult.getException(), newResult.getException()); + } + final SigningDetails newSigningDetails = newResult.getResult(); + + // Get signing details of the existing package + final PackageInfo existingApexPkg = mApexManager.getPackageInfo(packageName, + ApexManager.MATCH_ACTIVE_PACKAGE); + if (existingApexPkg == null) { + // This should never happen, because submitSessionToApexService ensures that no new + // apexes were installed. + throw new IllegalStateException("Unknown apex package " + packageName); + } + + final ParseResult<SigningDetails> existingResult = ApkSignatureVerifier.verify( + input.reset(), existingApexPkg.applicationInfo.sourceDir, + SigningDetails.SignatureSchemeVersion.JAR); + if (existingResult.isError()) { + throw new PackageManagerException(SessionInfo.STAGED_SESSION_VERIFICATION_FAILED, + "Failed to parse APEX package " + existingApexPkg.applicationInfo.sourceDir + + " : " + existingResult.getException(), existingResult.getException()); + } + final SigningDetails existingSigningDetails = existingResult.getResult(); + + // Verify signing details for upgrade + if (newSigningDetails.checkCapability(existingSigningDetails, + SigningDetails.CertCapabilities.INSTALLED_DATA) + || existingSigningDetails.checkCapability(newSigningDetails, + SigningDetails.CertCapabilities.ROLLBACK)) { + return; + } + + throw new PackageManagerException(SessionInfo.STAGED_SESSION_VERIFICATION_FAILED, + "APK-container signature of APEX package " + packageName + " with version " + + newApexPkg.versionCodeMajor + " and path " + apexPath + " is not" + + " compatible with the one currently installed on device"); + } + + private List<PackageInfo> submitSessionToApexService(StagingManager.StagedSession session, + int rollbackId) throws PackageManagerException { + final IntArray childSessionIds = new IntArray(); + if (session.isMultiPackage()) { + for (StagingManager.StagedSession s : session.getChildSessions()) { + if (s.isApexSession()) { + childSessionIds.add(s.sessionId()); + } + } + } + ApexSessionParams apexSessionParams = new ApexSessionParams(); + apexSessionParams.sessionId = session.sessionId(); + apexSessionParams.childSessionIds = childSessionIds.toArray(); + if (session.sessionParams().installReason == PackageManager.INSTALL_REASON_ROLLBACK) { + apexSessionParams.isRollback = true; + apexSessionParams.rollbackId = rollbackId; + } else { + if (rollbackId != -1) { + apexSessionParams.hasRollbackEnabled = true; + apexSessionParams.rollbackId = rollbackId; + } + } + // submitStagedSession will throw a PackageManagerException if apexd verification fails, + // which will be propagated to populate stagedSessionErrorMessage of this session. + final ApexInfoList apexInfoList = mApexManager.submitStagedSession(apexSessionParams); + final List<PackageInfo> result = new ArrayList<>(); + final List<String> apexPackageNames = new ArrayList<>(); + for (ApexInfo apexInfo : apexInfoList.apexInfos) { + final PackageInfo packageInfo; + final int flags = PackageManager.GET_META_DATA; + try (PackageParser2 packageParser = mPackageParserSupplier.get()) { + File apexFile = new File(apexInfo.modulePath); + final ParsedPackage parsedPackage = packageParser.parsePackage( + apexFile, flags, false); + packageInfo = PackageInfoWithoutStateUtils.generate(parsedPackage, apexInfo, flags); + if (packageInfo == null) { + throw new PackageManagerException( + SessionInfo.STAGED_SESSION_VERIFICATION_FAILED, + "Unable to generate package info: " + apexInfo.modulePath); + } + } catch (PackageManagerException e) { + throw new PackageManagerException(SessionInfo.STAGED_SESSION_VERIFICATION_FAILED, + "Failed to parse APEX package " + apexInfo.modulePath + " : " + e, e); + } + result.add(packageInfo); + apexPackageNames.add(packageInfo.packageName); + } + Slog.d(TAG, "Session " + session.sessionId() + " has following APEX packages: " + + apexPackageNames); + return result; + } + + private int retrieveRollbackIdForCommitSession(int sessionId) throws PackageManagerException { + RollbackManager rm = mContext.getSystemService(RollbackManager.class); + + final List<RollbackInfo> rollbacks = rm.getRecentlyCommittedRollbacks(); + for (int i = 0, size = rollbacks.size(); i < size; i++) { + final RollbackInfo rollback = rollbacks.get(i); + if (rollback.getCommittedSessionId() == sessionId) { + return rollback.getRollbackId(); + } + } + throw new PackageManagerException( + SessionInfo.STAGED_SESSION_VERIFICATION_FAILED, + "Could not find rollback id for commit session: " + sessionId); + } + + private static boolean isRollback(StagingManager.StagedSession session) { + return session.sessionParams().installReason == PackageManager.INSTALL_REASON_ROLLBACK; + } + + private static boolean isApexSessionFinalized(ApexSessionInfo info) { + /* checking if the session is in a final state, i.e., not active anymore */ + return info.isUnknown || info.isActivationFailed || info.isSuccess + || info.isReverted; + } + + private boolean ensureActiveApexSessionIsAborted(StagingManager.StagedSession session) { + if (!session.containsApexSession()) { + return true; + } + final int sessionId = session.sessionId(); + final ApexSessionInfo apexSession = mApexManager.getStagedSessionInfo(sessionId); + if (apexSession == null || isApexSessionFinalized(apexSession)) { + return true; + } + return mApexManager.abortStagedSession(sessionId); + } + + /** + * Checks if multiple staged-sessions are supported. It is supported only when the system + * supports checkpoint. + */ + private void checkActiveSessions() throws PackageManagerException { + final boolean supportsCheckpoint; + try { + supportsCheckpoint = PackageHelper.getStorageManager().supportsCheckpoint(); + } catch (RemoteException e) { + throw new PackageManagerException(SessionInfo.STAGED_SESSION_VERIFICATION_FAILED, + "Can't query fs-checkpoint status : " + e); + } + int activeSessions = 0; + for (StagingManager.StagedSession stagedSession : mStagedSessions) { + if (stagedSession.isDestroyed() || stagedSession.isInTerminalState()) { + continue; + } + ++activeSessions; + } + if (!supportsCheckpoint && activeSessions > 1) { + throw new PackageManagerException( + SessionInfo.STAGED_SESSION_VERIFICATION_FAILED, + "Cannot stage multiple sessions without checkpoint support"); + } + } + + /** + * Fails non-rollback sessions if any rollback session exists. A rollback session might cause + * downgrade of SDK extension which in turn will result in dependency violation of other + * non-rollback sessions. + */ + private void checkRollbacks(StagingManager.StagedSession session) + throws PackageManagerException { + for (StagingManager.StagedSession stagedSession : mStagedSessions) { + if (stagedSession.isDestroyed() || stagedSession.isInTerminalState()) { + continue; + } + if (isRollback(session) && !isRollback(stagedSession)) { + // If the new session is a rollback, then it gets priority. The existing + // session is failed to reduce risk and avoid an SDK extension dependency + // violation. + if (!ensureActiveApexSessionIsAborted(stagedSession)) { + Slog.e(TAG, "Failed to abort apex session " + stagedSession.sessionId()); + // Safe to ignore active apex session abort failure since session + // will be marked failed on next step and staging directory for session + // will be deleted. + } + stagedSession.setSessionFailed( + SessionInfo.STAGED_SESSION_CONFLICT, + "Session was failed by rollback session: " + session.sessionId()); + Slog.i(TAG, "Session " + stagedSession.sessionId() + " is marked failed due to " + + "rollback session: " + session.sessionId()); + } else if (!isRollback(session) && isRollback(stagedSession)) { + throw new PackageManagerException( + SessionInfo.STAGED_SESSION_CONFLICT, + "Session was failed by rollback session: " + stagedSession.sessionId()); + + } + } + } + + /** + * Fails the session that is committed later when overlapping packages are detected. + * + * @param parent The parent session. + * @param child The child session whose package name will be checked. + * This will equal to {@code parent} for a single-package session. + */ + private void checkOverlaps(StagingManager.StagedSession parent, + StagingManager.StagedSession child) throws PackageManagerException { + final String packageName = child.getPackageName(); + if (packageName == null) { + throw new PackageManagerException( + SessionInfo.STAGED_SESSION_VERIFICATION_FAILED, + "Cannot stage session " + child.sessionId() + " with package name null"); + } + for (StagingManager.StagedSession stagedSession : mStagedSessions) { + if (stagedSession.isDestroyed() || stagedSession.isInTerminalState() + || stagedSession.sessionId() == parent.sessionId()) { + continue; + } + if (stagedSession.sessionContains(s -> packageName.equals(s.getPackageName()))) { + if (stagedSession.getCommittedMillis() < parent.getCommittedMillis()) { + // Fail the session committed later when there are overlapping packages + throw new PackageManagerException( + SessionInfo.STAGED_SESSION_VERIFICATION_FAILED, + "Package: " + packageName + " in session: " + + child.sessionId() + + " has been staged already by session: " + + stagedSession.sessionId()); + } else { + stagedSession.setSessionFailed( + SessionInfo.STAGED_SESSION_VERIFICATION_FAILED, + "Package: " + packageName + " in session: " + + stagedSession.sessionId() + + " has been staged already by session: " + + child.sessionId()); + } + } + } + } +}
diff --git a/services/core/java/com/android/server/pm/StagingManager.java b/services/core/java/com/android/server/pm/StagingManager.java index a2f48c7..c10071b 100644 --- a/services/core/java/com/android/server/pm/StagingManager.java +++ b/services/core/java/com/android/server/pm/StagingManager.java
@@ -107,7 +107,6 @@ private final PowerManager mPowerManager; private final Context mContext; @VisibleForTesting - final PreRebootVerificationHandler mPreRebootVerificationHandler; private final Supplier<PackageParser2> mPackageParserSupplier; private final File mFailureReasonFile = new File("/metadata/staged-install/failure_reason.txt"); @@ -173,7 +172,6 @@ mApexManager = apexManager; mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); - mPreRebootVerificationHandler = new PreRebootVerificationHandler(looper); if (mFailureReasonFile.exists()) { try (BufferedReader reader = new BufferedReader(new FileReader(mFailureReasonFile))) { @@ -663,10 +661,16 @@ } } + @VisibleForTesting void commitSession(@NonNull StagedSession session) { - // Store this parent session which will be used to check overlapping later createSession(session); - mPreRebootVerificationHandler.startPreRebootVerification(session); + handleCommittedSession(session); + } + + private void handleCommittedSession(@NonNull StagedSession session) { + if (session.isSessionReady() && session.containsApexSession()) { + notifyStagedApexObservers(); + } } private int getSessionIdForParentOrSelf(StagedSession session) { @@ -1234,237 +1238,4 @@ } } } - - @VisibleForTesting - final class PreRebootVerificationHandler extends Handler { - - PreRebootVerificationHandler(Looper looper) { - super(looper); - } - - /** - * Handler for states of pre reboot verification. The states are arranged linearly (shown - * below) with each state either calling the next state, or calling some other method that - * eventually calls the next state. - * - * <p><ul> - * <li>MSG_PRE_REBOOT_VERIFICATION_START</li> - * <li>MSG_PRE_REBOOT_VERIFICATION_APEX</li> - * <li>MSG_PRE_REBOOT_VERIFICATION_END</li> - * </ul></p> - * - * Details about each of state can be found in corresponding handler of node. - */ - private static final int MSG_PRE_REBOOT_VERIFICATION_START = 1; - private static final int MSG_PRE_REBOOT_VERIFICATION_APEX = 2; - @VisibleForTesting - static final int MSG_PRE_REBOOT_VERIFICATION_END = 3; - - @Override - public void handleMessage(Message msg) { - final int sessionId = msg.arg1; - final int rollbackId = msg.arg2; - final StagedSession session = (StagedSession) msg.obj; - if (session.isDestroyed() || session.isSessionFailed()) { - // No point in running verification on a destroyed/failed session - onPreRebootVerificationComplete(session); - return; - } - try { - switch (msg.what) { - case MSG_PRE_REBOOT_VERIFICATION_START: - handlePreRebootVerification_Start(session); - break; - case MSG_PRE_REBOOT_VERIFICATION_APEX: - handlePreRebootVerification_Apex(session, rollbackId); - break; - case MSG_PRE_REBOOT_VERIFICATION_END: - handlePreRebootVerification_End(session); - break; - } - } catch (Exception e) { - Slog.e(TAG, "Pre-reboot verification failed due to unhandled exception", e); - onPreRebootVerificationFailure(session, - SessionInfo.STAGED_SESSION_ACTIVATION_FAILED, - "Pre-reboot verification failed due to unhandled exception: " + e); - } - } - - // Method for starting the pre-reboot verification - private synchronized void startPreRebootVerification( - @NonNull StagedSession session) { - mBootCompleted.thenRun(() -> { - int sessionId = session.sessionId(); - Slog.d(TAG, "Starting preRebootVerification for session " + sessionId); - obtainMessage(MSG_PRE_REBOOT_VERIFICATION_START, sessionId, -1, session) - .sendToTarget(); - }); - } - - private void onPreRebootVerificationFailure(StagedSession session, - @SessionInfo.StagedSessionErrorCode int errorCode, String errorMessage) { - if (!ensureActiveApexSessionIsAborted(session)) { - Slog.e(TAG, "Failed to abort apex session " + session.sessionId()); - // Safe to ignore active apex session abortion failure since session will be marked - // failed on next step and staging directory for session will be deleted. - } - session.setSessionFailed(errorCode, errorMessage); - onPreRebootVerificationComplete(session); - } - - // Things to do when pre-reboot verification completes for a particular sessionId - private void onPreRebootVerificationComplete(StagedSession session) { - int sessionId = session.sessionId(); - Slog.d(TAG, "Stopping preRebootVerification for session " + sessionId); - session.notifyEndPreRebootVerification(); - } - - private void notifyPreRebootVerification_Start_Complete( - @NonNull StagedSession session, int rollbackId) { - obtainMessage(MSG_PRE_REBOOT_VERIFICATION_APEX, session.sessionId(), rollbackId, - session).sendToTarget(); - } - - private void notifyPreRebootVerification_Apex_Complete( - @NonNull StagedSession session) { - obtainMessage(MSG_PRE_REBOOT_VERIFICATION_END, session.sessionId(), -1, session) - .sendToTarget(); - } - - /** - * A placeholder state for starting the pre reboot verification. - * - * See {@link PreRebootVerificationHandler} to see all nodes of pre reboot verification - */ - private void handlePreRebootVerification_Start(@NonNull StagedSession session) { - try { - if (session.isMultiPackage()) { - for (StagedSession s : session.getChildSessions()) { - checkNonOverlappingWithStagedSessions(s); - } - } else { - checkNonOverlappingWithStagedSessions(session); - } - } catch (PackageManagerException e) { - onPreRebootVerificationFailure(session, e.error, e.getMessage()); - return; - } - - int rollbackId = -1; - if ((session.sessionParams().installFlags & PackageManager.INSTALL_ENABLE_ROLLBACK) - != 0) { - // If rollback is enabled for this session, we call through to the RollbackManager - // with the list of sessions it must enable rollback for. Note that - // notifyStagedSession is a synchronous operation. - final RollbackManagerInternal rm = - LocalServices.getService(RollbackManagerInternal.class); - try { - // NOTE: To stay consistent with the non-staged install flow, we don't fail the - // entire install if rollbacks can't be enabled. - rollbackId = rm.notifyStagedSession(session.sessionId()); - } catch (RuntimeException re) { - Slog.e(TAG, "Failed to notifyStagedSession for session: " - + session.sessionId(), re); - } - } else if (session.sessionParams().installReason - == PackageManager.INSTALL_REASON_ROLLBACK) { - try { - rollbackId = retrieveRollbackIdForCommitSession(session.sessionId()); - } catch (PackageManagerException e) { - onPreRebootVerificationFailure(session, e.error, e.getMessage()); - return; - } - } - - notifyPreRebootVerification_Start_Complete(session, rollbackId); - } - - /** - * Pre-reboot verification state for apex files: - * - * <p><ul> - * <li>submits session to apex service</li> - * <li>validates signatures of apex files</li> - * </ul></p> - */ - private void handlePreRebootVerification_Apex( - @NonNull StagedSession session, int rollbackId) { - final boolean hasApex = session.containsApexSession(); - - // APEX checks. For single-package sessions, check if they contain an APEX. For - // multi-package sessions, find all the child sessions that contain an APEX. - if (hasApex) { - final List<PackageInfo> apexPackages; - try { - apexPackages = submitSessionToApexService(session, rollbackId); - for (int i = 0, size = apexPackages.size(); i < size; i++) { - validateApexSignature(apexPackages.get(i)); - } - } catch (PackageManagerException e) { - onPreRebootVerificationFailure(session, e.error, e.getMessage()); - return; - } - - final PackageManagerInternal packageManagerInternal = - LocalServices.getService(PackageManagerInternal.class); - packageManagerInternal.pruneCachedApksInApex(apexPackages); - } - - notifyPreRebootVerification_Apex_Complete(session); - } - - /** - * Pre-reboot verification state for wrapping up: - * <p><ul> - * <li>enables rollback if required</li> - * <li>marks session as ready</li> - * </ul></p> - */ - private void handlePreRebootVerification_End(@NonNull StagedSession session) { - // Before marking the session as ready, start checkpoint service if available - try { - if (PackageHelper.getStorageManager().supportsCheckpoint()) { - PackageHelper.getStorageManager().startCheckpoint(2); - } - } catch (Exception e) { - // Failed to get hold of StorageManager - Slog.e(TAG, "Failed to get hold of StorageManager", e); - onPreRebootVerificationFailure(session, SessionInfo.STAGED_SESSION_UNKNOWN, - "Failed to get hold of StorageManager"); - return; - } - - // Stop pre-reboot verification before marking session ready. From this point on, if we - // abandon the session then it will be cleaned up immediately. If session is abandoned - // after this point, then even if for some reason system tries to install the session - // or activate its apex, there won't be any files to work with as they will be cleaned - // up by the system as part of abandonment. If session is abandoned before this point, - // then the session is already destroyed and cannot be marked ready anymore. - onPreRebootVerificationComplete(session); - - // Proactively mark session as ready before calling apexd. Although this call order - // looks counter-intuitive, this is the easiest way to ensure that session won't end up - // in the inconsistent state: - // - If device gets rebooted right before call to apexd, then apexd will never activate - // apex files of this staged session. This will result in StagingManager failing - // the session. - // On the other hand, if the order of the calls was inverted (first call apexd, then - // mark session as ready), then if a device gets rebooted right after the call to apexd, - // only apex part of the train will be applied, leaving device in an inconsistent state. - Slog.d(TAG, "Marking session " + session.sessionId() + " as ready"); - session.setSessionReady(); - if (session.isSessionReady()) { - final boolean hasApex = session.containsApexSession(); - if (hasApex) { - try { - mApexManager.markStagedSessionReady(session.sessionId()); - notifyStagedApexObservers(); - } catch (PackageManagerException e) { - session.setSessionFailed(e.error, e.getMessage()); - return; - } - } - } - } - } }
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java index 6a4d69f..776bf8b 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java
@@ -726,10 +726,9 @@ { FakeStagedSession session = new FakeStagedSession(239); session.setIsApex(true); - mStagingManager.createSession(session); - + session.setSessionReady(); mockApexManagerGetStagedApexInfoWithSessionId(); - triggerEndOfPreRebootVerification(session); + mStagingManager.commitSession(session); assertThat(session.isSessionReady()).isTrue(); ArgumentCaptor<ApexStagedEvent> argumentCaptor = ArgumentCaptor.forClass( @@ -744,9 +743,8 @@ Mockito.clearInvocations(observer); FakeStagedSession session = new FakeStagedSession(240); session.setIsApex(true); - mStagingManager.createSession(session); - - triggerEndOfPreRebootVerification(session); + session.setSessionReady(); + mStagingManager.commitSession(session); assertThat(session.isSessionReady()).isTrue(); ArgumentCaptor<ApexStagedEvent> argumentCaptor = ArgumentCaptor.forClass( @@ -762,9 +760,8 @@ Mockito.clearInvocations(observer); FakeStagedSession session = new FakeStagedSession(241); session.setIsApex(true); - mStagingManager.createSession(session); - - triggerEndOfPreRebootVerification(session); + session.setSessionReady(); + mStagingManager.commitSession(session); assertThat(session.isSessionReady()).isTrue(); verify(observer, never()).onApexStaged(any()); @@ -800,21 +797,13 @@ // Trigger end of pre-reboot verification FakeStagedSession session = new FakeStagedSession(239); - mStagingManager.createSession(session); + session.setSessionReady(); + mStagingManager.commitSession(session); - triggerEndOfPreRebootVerification(session); assertThat(session.isSessionReady()).isTrue(); verify(observer, never()).onApexStaged(any()); } - private void triggerEndOfPreRebootVerification(StagingManager.StagedSession session) { - StagingManager.PreRebootVerificationHandler handler = - mStagingManager.mPreRebootVerificationHandler; - Message msg = handler.obtainMessage( - handler.MSG_PRE_REBOOT_VERIFICATION_END, session.sessionId(), -1, session); - handler.handleMessage(msg); - } - private StagingManager.StagedSession createSession(int sessionId, String packageName, long committedMillis) { PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(