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

Commit db989230 authored by Nikita Ioffe's avatar Nikita Ioffe
Browse files

Reject non-staged APEX install if there is staged install of same APEX

There is a interesting interaction between staged and non-staged
installs of the same APEX. Let's say an installer staged v1 -> v2 APEX
update, and then does a non-staged update to v3. After device is
rebooted, apexd will apply the staged v1 -> v2 session, silently
downgrading an APEX from v3.

For apks, this problem is solved by storing an expected version. When an
APK session is being applied during boot, Package Manager will check if
the currently installed version is equal to the expected one stored in
the staged session. If they mismatch, an install is failed.
Unfortunately, implementing the same logic in apexd will require a
non-trivial refactoring which is too late to do in S. Instead we are
just going to fail the non-staged installation.

Test: atest StagedInstallInternalTest
Bug: 187864524
Change-Id: I9000f40cede9a324a5059a09deb8eb5be13b21f9
parent e501fd83
Loading
Loading
Loading
Loading
+15 −0
Original line number Diff line number Diff line
@@ -2260,6 +2260,21 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
                    return;
                }
            }

            if (!params.isStaged) {
                // For non-staged APEX installs also check if there is a staged session that
                // contains the same APEX. If that's the case, we should fail this session.
                synchronized (mLock) {
                    int sessionId = mStagingManager.getSessionIdByPackageName(mPackageName);
                    if (sessionId != -1) {
                        onSessionValidationFailure(
                                PackageManager.INSTALL_FAILED_VERIFICATION_FAILURE,
                                "Staged session " + sessionId + " already contains "
                                        + mPackageName);
                        return;
                    }
                }
            }
        }

        if (params.isStaged) {
+20 −0
Original line number Diff line number Diff line
@@ -777,6 +777,26 @@ public class StagingManager {
        }
    }

    /**
     * Returns id of a committed and non-finalized stated session that contains same
     * {@code packageName}, or {@code -1} if no sessions have this {@code packageName} staged.
     */
    int getSessionIdByPackageName(@NonNull String packageName) {
        synchronized (mStagedSessions) {
            for (int i = 0; i < mStagedSessions.size(); i++) {
                StagedSession stagedSession = mStagedSessions.valueAt(i);
                if (!stagedSession.isCommitted() || stagedSession.isDestroyed()
                        || stagedSession.isInTerminalState()) {
                    continue;
                }
                if (stagedSession.getPackageName().equals(packageName)) {
                    return stagedSession.sessionId();
                }
            }
        }
        return -1;
    }

    @VisibleForTesting
    void createSession(@NonNull StagedSession sessionInfo) {
        synchronized (mStagedSessions) {
+60 −0
Original line number Diff line number Diff line
@@ -459,6 +459,66 @@ public class StagingManagerTest {
        assertThat(apkSession.getErrorMessage()).isEqualTo("Another apex session failed");
    }

    @Test
    public void getSessionIdByPackageName() throws Exception {
        FakeStagedSession session = new FakeStagedSession(239);
        session.setCommitted(true);
        session.setSessionReady();
        session.setPackageName("com.foo");

        mStagingManager.createSession(session);
        assertThat(mStagingManager.getSessionIdByPackageName("com.foo")).isEqualTo(239);
    }

    @Test
    public void getSessionIdByPackageName_appliedSession_ignores() throws Exception {
        FakeStagedSession session = new FakeStagedSession(37);
        session.setCommitted(true);
        session.setSessionApplied();
        session.setPackageName("com.foo");

        mStagingManager.createSession(session);
        assertThat(mStagingManager.getSessionIdByPackageName("com.foo")).isEqualTo(-1);
    }

    @Test
    public void getSessionIdByPackageName_failedSession_ignores() throws Exception {
        FakeStagedSession session = new FakeStagedSession(73);
        session.setCommitted(true);
        session.setSessionFailed(1, "whatevs");
        session.setPackageName("com.foo");

        mStagingManager.createSession(session);
        assertThat(mStagingManager.getSessionIdByPackageName("com.foo")).isEqualTo(-1);
    }

    @Test
    public void getSessionIdByPackageName_destroyedSession_ignores() throws Exception {
        FakeStagedSession session = new FakeStagedSession(23);
        session.setCommitted(true);
        session.setDestroyed(true);
        session.setPackageName("com.foo");

        mStagingManager.createSession(session);
        assertThat(mStagingManager.getSessionIdByPackageName("com.foo")).isEqualTo(-1);
    }

    @Test
    public void getSessionIdByPackageName_noSessions() throws Exception {
        assertThat(mStagingManager.getSessionIdByPackageName("com.foo")).isEqualTo(-1);
    }

    @Test
    public void getSessionIdByPackageName_noSessionHasThisPackage() throws Exception {
        FakeStagedSession session = new FakeStagedSession(37);
        session.setCommitted(true);
        session.setSessionApplied();
        session.setPackageName("com.foo");

        mStagingManager.createSession(session);
        assertThat(mStagingManager.getSessionIdByPackageName("com.bar")).isEqualTo(-1);
    }

    private StagingManager.StagedSession createSession(int sessionId, String packageName,
            long committedMillis) {
        PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
+1 −0
Original line number Diff line number Diff line
@@ -32,6 +32,7 @@ android_test_helper_app {
    test_suites: ["general-tests"],
    java_resources: [
        ":com.android.apex.apkrollback.test_v2",
        ":StagedInstallTestApexV2",
        ":StagedInstallTestApexV2_WrongSha",
        ":test.rebootless_apex_v1",
        ":test.rebootless_apex_v2",
+16 −0
Original line number Diff line number Diff line
@@ -57,6 +57,9 @@ public class StagedInstallInternalTest {
    private static final TestApp APEX_WRONG_SHA_V2 = new TestApp(
            "ApexWrongSha2", SHIM_APEX_PACKAGE_NAME, 2, /* isApex= */ true,
            "com.android.apex.cts.shim.v2_wrong_sha.apex");
    private static final TestApp APEX_V2 = new TestApp(
            "ApexV2", SHIM_APEX_PACKAGE_NAME, 2, /* isApex= */ true,
            "com.android.apex.cts.shim.v2.apex");

    private File mTestStateFile = new File(
            InstrumentationRegistry.getInstrumentation().getContext().getFilesDir(),
@@ -388,6 +391,19 @@ public class StagedInstallInternalTest {
        }
    }

    @Test
    public void testRebootlessUpdate_hasStagedSessionWithSameApex_fails() throws Exception {
        assertThat(InstallUtils.getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(1);

        int sessionId = Install.single(APEX_V2).setStaged().commit();
        assertSessionReady(sessionId);
        InstallUtils.commitExpectingFailure(
                AssertionError.class,
                "Staged session " + sessionId + " already contains " + SHIM_APEX_PACKAGE_NAME,
                Install.single(APEX_V2));

    }

    private static void assertSessionApplied(int sessionId) {
        assertSessionState(sessionId, (session) -> {
            assertThat(session.isStagedSessionApplied()).isTrue();
Loading