Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 015f9351 authored by Dario Freni's avatar Dario Freni
Browse files

Support for multi-package apex sessions.

If we have multi-package sessions, we check whether they contain APEX
packages and proceed with the pre-reboot verification of the whole
session. Session is then applied by apexd on reboot, and the state is
recovered and updated by StagingManager as soon as the sessions are
restored.

Bug: 118865310
Test: adb install-multi-package file1.apex file2.apex; verified sessions
are written correctly, restored correctly on stop/start, applied
correctly at reboot and their state is updated.
Test: atest apex_e2e_tests (to make sure we didn't regress)
Change-Id: Ia31d5badace016074d158e31c574fbad81576984
parent 9f6cfc33
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -1132,6 +1132,7 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements

        public void onStagedSessionChanged(PackageInstallerSession session) {
            writeSessionsAsync();
            // TODO(b/118865310): don't send broadcast if system is not ready.
            mPm.sendSessionUpdatedBroadcast(session.generateInfo(false), session.userId);
        }

+23 −68
Original line number Diff line number Diff line
@@ -992,8 +992,12 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {

        // Read transfers from the original owner stay open, but as the session's data
        // cannot be modified anymore, there is no leak of information. For staged sessions,
        // further validation may be performed by the staging manager.
        // further validation is performed by the staging manager.
        if (!params.isMultiPackage) {
            if ((params.installFlags & PackageManager.INSTALL_APEX) != 0) {
                // For APEX, validation is done by StagingManager post-commit.
                return;
            }
            final PackageInfo pkgInfo = mPm.getPackageInfo(
                    params.appPackageName, PackageManager.GET_SIGNATURES
                            | PackageManager.MATCH_STATIC_SHARED_LIBRARIES /*flags*/, userId);
@@ -1001,16 +1005,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
            resolveStageDirLocked();

            try {
                if ((params.installFlags & PackageManager.INSTALL_APEX) != 0) {
                    // TODO(b/118865310): Remove this when APEX validation is done via
                    //                    StagingManager.
                    validateApexInstallLocked(pkgInfo);
                } else {
                    // Verify that stage looks sane with respect to existing application.
                    // This currently only ensures packageName, versionCode, and certificate
                    // consistency.
                validateApkInstallLocked(pkgInfo);
                }
            } catch (PackageManagerException e) {
                throw e;
            } catch (Throwable e) {
@@ -1301,54 +1296,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
                (params.installFlags & PackageManager.DONT_KILL_APP) != 0;
    }

    @GuardedBy("mLock")
    private void validateApexInstallLocked(@Nullable PackageInfo pkgInfo)
            throws PackageManagerException {
        mResolvedStagedFiles.clear();
        mResolvedInheritedFiles.clear();

        try {
            resolveStageDirLocked();
        } catch (IOException e) {
            throw new PackageManagerException(INSTALL_FAILED_CONTAINER_ERROR,
                "Failed to resolve stage location", e);
        }

        final File[] addedFiles = mResolvedStageDir.listFiles(sAddedFilter);
        if (ArrayUtils.isEmpty(addedFiles)) {
            throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, "No packages staged");
        }

        if (addedFiles.length > 1) {
            throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
                "Only one APEX file at a time might be installed");
        }
        File addedFile = addedFiles[0];
        final ApkLite apk;
        try {
            apk = PackageParser.parseApkLite(
                addedFile, PackageParser.PARSE_COLLECT_CERTIFICATES);
        } catch (PackageParserException e) {
            throw PackageManagerException.from(e);
        }

        mPackageName = apk.packageName;
        mVersionCode = apk.getLongVersionCode();
        mSigningDetails = apk.signingDetails;
        mResolvedBaseFile = addedFile;

        // STOPSHIP: Ensure that we remove the non-staged version of APEX installs in production
        // because we currently do not verify that signatures are consistent with the previously
        // installed version in that case.
        //
        // When that happens, this hack can be reverted and we can rely on APEXd to map between
        // APEX files and their package names instead of parsing it out of the AndroidManifest
        // such as here.
        if (params.appPackageName == null) {
            params.appPackageName = mPackageName;
        }
    }

    /**
     * Validate install by confirming that all application packages are have
     * consistent package name, version code, and signing certificates.
@@ -1911,22 +1858,30 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
    }

    @Override
    public void addChildSessionId(int sessionId) {
        final PackageInstallerSession session = mSessionProvider.getSession(sessionId);
        if (session == null) {
    public void addChildSessionId(int childSessionId) {
        final PackageInstallerSession childSession = mSessionProvider.getSession(childSessionId);
        if (childSession == null) {
            throw new RemoteException("Unable to add child.",
                    new PackageManagerException("Child session " + sessionId + " does not exist"),
                    new PackageManagerException("Child session " + childSessionId
                            + " does not exist"),
                    false, true).rethrowAsRuntimeException();
        }
        // Session groups must be consistent wrt to isStaged parameter. Non-staging session
        // cannot be grouped with staging sessions.
        if (this.params.isStaged ^ childSession.params.isStaged) {
            throw new RemoteException("Unable to add child.",
                    new PackageManagerException("Child session " + childSessionId
                            + " and parent session " + this.sessionId + " do not have consistent"
                            + " staging session settings."),
                    false, true).rethrowAsRuntimeException();
        }
        synchronized (mLock) {
            final int indexOfSession = mChildSessionIds.indexOfKey(sessionId);
            final int indexOfSession = mChildSessionIds.indexOfKey(childSessionId);
            if (indexOfSession >= 0) {
                return;
            }
            session.setParentSessionId(this.sessionId);
            // TODO: sanity check, if parent session is staged then child session should be
            //       marked as staged.
            addChildSessionIdInternal(sessionId);
            childSession.setParentSessionId(this.sessionId);
            addChildSessionIdInternal(childSessionId);
        }
    }

+104 −26
Original line number Diff line number Diff line
@@ -41,7 +41,9 @@ import com.android.internal.annotations.GuardedBy;
import com.android.internal.os.BackgroundThread;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

/**
 * This class handles staged install sessions, i.e. install sessions that require packages to
@@ -126,12 +128,24 @@ public class StagingManager {
        return false;
    }

    private static boolean submitSessionToApexService(int sessionId, ApexInfoList apexInfoList) {
    private static boolean submitSessionToApexService(@NonNull PackageInstallerSession session,
                                                      List<PackageInstallerSession> childSessions,
                                                      ApexInfoList apexInfoList) {
        return sendSubmitStagedSessionRequest(
                session.sessionId,
                childSessions != null
                        ? childSessions.stream().mapToInt(s -> s.sessionId).toArray() :
                        new int[]{},
                apexInfoList);
    }

    private static boolean sendSubmitStagedSessionRequest(
            int sessionId, int[] childSessionIds, ApexInfoList apexInfoList) {
        final IApexService apex = IApexService.Stub.asInterface(
                ServiceManager.getService("apexservice"));
        boolean success;
        try {
            success = apex.submitStagedSession(sessionId, new int[0], apexInfoList);
            success = apex.submitStagedSession(sessionId, childSessionIds, apexInfoList);
        } catch (RemoteException re) {
            Slog.e(TAG, "Unable to contact apexservice", re);
            return false;
@@ -139,22 +153,40 @@ public class StagingManager {
        return success;
    }

    private static boolean isApexSession(@NonNull PackageInstallerSession session) {
        return (session.params.installFlags & PackageManager.INSTALL_APEX) != 0;
    }

    private void preRebootVerification(@NonNull PackageInstallerSession session) {
        boolean success = true;
        if ((session.params.installFlags & PackageManager.INSTALL_APEX) != 0) {

        final ApexInfoList apexInfoList = new ApexInfoList();
        // 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 (!session.isMultiPackage()
                && isApexSession(session)) {
            success = submitSessionToApexService(session, null, apexInfoList);
        } else if (session.isMultiPackage()) {
            List<PackageInstallerSession> childSessions =
                    Arrays.stream(session.getChildSessionIds())
                            // Retrieve cached sessions matching ids.
                            .mapToObj(i -> mStagedSessions.get(i))
                            // Filter only the ones containing APEX.
                            .filter(childSession -> isApexSession(childSession))
                            .collect(Collectors.toList());
            if (!childSessions.isEmpty()) {
                success = submitSessionToApexService(session, childSessions, apexInfoList);
            } // else this is a staged multi-package session with no APEX files.
        }

            if (!submitSessionToApexService(session.sessionId, apexInfoList)) {
                success = false;
            } else {
        if (success && (apexInfoList.apexInfos.length > 0)) {
            // For APEXes, we validate the signature here before we mark the session as ready,
            // so we fail the session early if there is a signature mismatch. For APKs, the
            // signature verification will be done by the package manager at the point at which
            // it applies the staged install.
            //
                // TODO: Decide whether we want to fail fast by detecting signature mismatches right
                // away.
            // TODO: Decide whether we want to fail fast by detecting signature mismatches for APKs,
            // right away.
            for (ApexInfo apexPackage : apexInfoList.apexInfos) {
                if (!validateApexSignatureLocked(apexPackage.packagePath,
                        apexPackage.packageName)) {
@@ -163,7 +195,7 @@ public class StagingManager {
                }
            }
        }
        }

        if (success) {
            session.setStagedSessionReady();
        } else {
@@ -206,15 +238,59 @@ public class StagingManager {
        }
    }

    void abortSession(@NonNull PackageInstallerSession sessionInfo) {
        updateStoredSession(sessionInfo);
    void abortSession(@NonNull PackageInstallerSession session) {
        synchronized (mStagedSessions) {
            mStagedSessions.remove(sessionInfo.sessionId);
            updateStoredSession(session);
            mStagedSessions.remove(session.sessionId);
        }
    }

    @GuardedBy("mStagedSessions")
    private boolean isMultiPackageSessionComplete(@NonNull PackageInstallerSession session) {
        // This method assumes that the argument is either a parent session of a multi-package
        // i.e. isMultiPackage() returns true, or that it is a child session, i.e.
        // hasParentSessionId() returns true.
        if (session.isMultiPackage()) {
            // Parent session of a multi-package group. Check that we restored all the children.
            for (int childSession : session.getChildSessionIds()) {
                if (mStagedSessions.get(childSession) == null) {
                    return false;
                }
            }
            return true;
        }
        if (session.hasParentSessionId()) {
            PackageInstallerSession parent = mStagedSessions.get(session.getParentSessionId());
            if (parent == null) {
                return false;
            }
            return isMultiPackageSessionComplete(parent);
        }
        Slog.wtf(TAG, "Attempting to restore an invalid multi-package session.");
        return false;
    }

    void restoreSession(@NonNull PackageInstallerSession session) {
        updateStoredSession(session);
        PackageInstallerSession sessionToResume = session;
        synchronized (mStagedSessions) {
            mStagedSessions.append(session.sessionId, session);
            // For multi-package sessions, we don't know in which order they will be restored. We
            // need to wait until we have restored all the session in a group before restoring them.
            if (session.isMultiPackage() || session.hasParentSessionId()) {
                if (!isMultiPackageSessionComplete(session)) {
                    // Still haven't recovered all sessions of the group, return.
                    return;
                }
                // Group recovered, find the parent if necessary and resume the installation.
                if (session.hasParentSessionId()) {
                    sessionToResume = mStagedSessions.get(session.getParentSessionId());
                }
            }
        }
        checkStateAndResume(sessionToResume);
    }

    private void checkStateAndResume(@NonNull PackageInstallerSession session) {
        // Check the state of the session and decide what to do next.
        if (session.isStagedSessionFailed() || session.isStagedSessionApplied()) {
            // Final states, nothing to do.
@@ -227,6 +303,8 @@ public class StagingManager {
        } else {
            // Session had already being marked ready. Start the checks to verify if there is any
            // follow-up work.
            // TODO(b/118865310): should this be synchronous to ensure it completes before
            //                    systemReady() finishes?
            mBgHandler.post(() -> resumeSession(session));
        }
    }