From db9892300e9233557d47cf43c5975357419a9d8c Mon Sep 17 00:00:00 2001 From: Nikita Ioffe Date: Thu, 22 Jul 2021 01:37:20 +0100 Subject: [PATCH] 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 --- .../server/pm/PackageInstallerSession.java | 15 +++++ .../com/android/server/pm/StagingManager.java | 20 +++++++ .../android/server/pm/StagingManagerTest.java | 60 +++++++++++++++++++ tests/StagedInstallTest/Android.bp | 1 + .../StagedInstallInternalTest.java | 16 +++++ .../host/StagedInstallInternalTest.java | 8 +++ 6 files changed, 120 insertions(+) diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index 7cc49fd7aab24..542948491dc8c 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -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) { diff --git a/services/core/java/com/android/server/pm/StagingManager.java b/services/core/java/com/android/server/pm/StagingManager.java index 4ac5be2ec7c7c..bdbcb277e8d66 100644 --- a/services/core/java/com/android/server/pm/StagingManager.java +++ b/services/core/java/com/android/server/pm/StagingManager.java @@ -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) { 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 f91cb2801bc12..521be70df633f 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java @@ -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( diff --git a/tests/StagedInstallTest/Android.bp b/tests/StagedInstallTest/Android.bp index 1aa04996f6827..cac14a72a7061 100644 --- a/tests/StagedInstallTest/Android.bp +++ b/tests/StagedInstallTest/Android.bp @@ -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", diff --git a/tests/StagedInstallTest/app/src/com/android/tests/stagedinstallinternal/StagedInstallInternalTest.java b/tests/StagedInstallTest/app/src/com/android/tests/stagedinstallinternal/StagedInstallInternalTest.java index 60585e84d4ef5..4684f0182d034 100644 --- a/tests/StagedInstallTest/app/src/com/android/tests/stagedinstallinternal/StagedInstallInternalTest.java +++ b/tests/StagedInstallTest/app/src/com/android/tests/stagedinstallinternal/StagedInstallInternalTest.java @@ -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(); diff --git a/tests/StagedInstallTest/src/com/android/tests/stagedinstallinternal/host/StagedInstallInternalTest.java b/tests/StagedInstallTest/src/com/android/tests/stagedinstallinternal/host/StagedInstallInternalTest.java index c6b6aabb74ace..5021009f65ae8 100644 --- a/tests/StagedInstallTest/src/com/android/tests/stagedinstallinternal/host/StagedInstallInternalTest.java +++ b/tests/StagedInstallTest/src/com/android/tests/stagedinstallinternal/host/StagedInstallInternalTest.java @@ -470,6 +470,14 @@ public class StagedInstallInternalTest extends BaseHostJUnit4Test { runPhase("testRebootlessUpdates"); } + @Test + public void testRebootlessUpdate_hasStagedSessionWithSameApex_fails() throws Exception { + assumeTrue("Device does not support updating APEX", + mHostUtils.isApexUpdateSupported()); + + runPhase("testRebootlessUpdate_hasStagedSessionWithSameApex_fails"); + } + private List getStagingDirectories() throws DeviceNotAvailableException { String baseDir = "/data/app-staging"; try {