From ac7bc339b2e63cd25594536481cd78d52da9141a Mon Sep 17 00:00:00 2001 From: Oli Lan Date: Fri, 24 Apr 2020 16:01:41 +0100 Subject: [PATCH] Block rollback commit if it may violate min extension constraint. This CL adds a check when a rollback is committed, if the rollback contains an apex and an extension version has increased since the rollback was created. The check looks for any installed app with a minExtensionVersion greater than was present when the rollback was created. If such an app exists, the rollback commit is not allowed to occur. See go/sdk-extensions-and-rollback for more details. Bug: 152737927 Test: atest RollbackUnitTest Test: atest RollbackTest Test: atest RollbackManagerHostTest Merged-In: I7fd7d98a03ecb1edb1b79475fadb944bc4f2fd6f Change-Id: I7fd7d98a03ecb1edb1b79475fadb944bc4f2fd6f --- .../com/android/server/rollback/Rollback.java | 80 +++++++++++++++- .../server/rollback/RollbackStore.java | 9 +- .../server/rollback/RollbackStoreTest.java | 7 +- .../server/rollback/RollbackUnitTest.java | 93 +++++++++++++++++++ 4 files changed, 175 insertions(+), 14 deletions(-) diff --git a/services/core/java/com/android/server/rollback/Rollback.java b/services/core/java/com/android/server/rollback/Rollback.java index 9171501270d04..a0f40708627e9 100644 --- a/services/core/java/com/android/server/rollback/Rollback.java +++ b/services/core/java/com/android/server/rollback/Rollback.java @@ -27,6 +27,7 @@ import android.content.Intent; import android.content.IntentSender; import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; +import android.content.pm.PackageManagerInternal; import android.content.pm.UserInfo; import android.content.pm.VersionedPackage; import android.content.rollback.PackageRollbackInfo; @@ -36,6 +37,7 @@ import android.os.Binder; import android.os.ParcelFileDescriptor; import android.os.UserHandle; import android.os.UserManager; +import android.os.ext.SdkExtensions; import android.text.TextUtils; import android.util.IntArray; import android.util.Slog; @@ -43,8 +45,11 @@ import android.util.SparseIntArray; import android.util.SparseLongArray; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.internal.util.IndentingPrintWriter; +import com.android.server.LocalServices; +import com.android.server.pm.parsing.pkg.AndroidPackage; import java.io.File; import java.io.IOException; @@ -175,10 +180,9 @@ class Rollback { private int mNumPackageSessionsWithSuccess; /** - * The extension versions supported at the time of rollback creation. May be null if not set - * at creation time. + * The extension versions supported at the time of rollback creation. */ - @Nullable private final SparseIntArray mExtensionVersions; + private final SparseIntArray mExtensionVersions; /** * Constructs a new, empty Rollback instance. @@ -211,7 +215,8 @@ class Rollback { Rollback(int rollbackId, File backupDir, int stagedSessionId, int userId, String installerPackageName) { - this(rollbackId, backupDir, stagedSessionId, userId, installerPackageName, null, null); + this(rollbackId, backupDir, stagedSessionId, userId, installerPackageName, null, + new SparseIntArray(0)); } /** @@ -297,7 +302,7 @@ class Rollback { * Returns the extension versions that were supported at the time that the rollback was created, * as a mapping from SdkVersion to ExtensionVersion. */ - @Nullable SparseIntArray getExtensionVersions() { + SparseIntArray getExtensionVersions() { return mExtensionVersions; } @@ -470,6 +475,15 @@ class Rollback { return; } + if (containsApex() && wasCreatedAtLowerExtensionVersion()) { + PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class); + if (extensionVersionReductionWouldViolateConstraint(mExtensionVersions, pmi)) { + sendFailure(context, statusReceiver, RollbackManager.STATUS_FAILURE, + "Rollback may violate a minExtensionVersion constraint"); + return; + } + } + // Get a context to use to install the downgraded version of the package. Context pkgContext; try { @@ -845,6 +859,56 @@ class Rollback { } } + /** + * Returns true if there is an app installed that specifies a minExtensionVersion greater + * than what was present at the time this Rollback was created. + */ + @VisibleForTesting + static boolean extensionVersionReductionWouldViolateConstraint( + SparseIntArray rollbackExtVers, PackageManagerInternal pmi) { + if (rollbackExtVers.size() == 0) { + return false; + } + List packages = pmi.getPackageList().getPackageNames(); + for (int i = 0; i < packages.size(); i++) { + AndroidPackage pkg = pmi.getPackage(packages.get(i)); + SparseIntArray minExtVers = pkg.getMinExtensionVersions(); + if (minExtVers == null) { + continue; + } + for (int j = 0; j < rollbackExtVers.size(); j++) { + int minExt = minExtVers.get(rollbackExtVers.keyAt(j), -1); + if (rollbackExtVers.valueAt(j) < minExt) { + return true; + } + } + } + return false; + } + + /** + * Returns true if for any SDK version, the extension version recorded at the time of rollback + * creation is lower than the current extension version. + */ + private boolean wasCreatedAtLowerExtensionVersion() { + for (int i = 0; i < mExtensionVersions.size(); i++) { + if (SdkExtensions.getExtensionVersion(mExtensionVersions.keyAt(i)) + > mExtensionVersions.valueAt(i)) { + return true; + } + } + return false; + } + + private boolean containsApex() { + for (PackageRollbackInfo pkgInfo : info.getPackages()) { + if (pkgInfo.isApex()) { + return true; + } + } + return false; + } + void dump(IndentingPrintWriter ipw) { synchronized (mLock) { ipw.println(info.getRollbackId() + ":"); @@ -871,6 +935,12 @@ class Rollback { ipw.decreaseIndent(); ipw.println("-committedSessionId: " + info.getCommittedSessionId()); } + if (mExtensionVersions.size() > 0) { + ipw.println("-extensionVersions:"); + ipw.increaseIndent(); + ipw.println(mExtensionVersions.toString()); + ipw.decreaseIndent(); + } ipw.decreaseIndent(); } } diff --git a/services/core/java/com/android/server/rollback/RollbackStore.java b/services/core/java/com/android/server/rollback/RollbackStore.java index f186d65baaa19..792cc45ee1808 100644 --- a/services/core/java/com/android/server/rollback/RollbackStore.java +++ b/services/core/java/com/android/server/rollback/RollbackStore.java @@ -179,10 +179,7 @@ class RollbackStore { } private static @Nullable JSONArray extensionVersionsToJson( - @Nullable SparseIntArray extensionVersions) throws JSONException { - if (extensionVersions == null) { - return null; - } + SparseIntArray extensionVersions) throws JSONException { JSONArray array = new JSONArray(); for (int i = 0; i < extensionVersions.size(); i++) { JSONObject entryJson = new JSONObject(); @@ -193,10 +190,10 @@ class RollbackStore { return array; } - private static @Nullable SparseIntArray extensionVersionsFromJson(@Nullable JSONArray json) + private static @Nullable SparseIntArray extensionVersionsFromJson(JSONArray json) throws JSONException { if (json == null) { - return null; + return new SparseIntArray(0); } SparseIntArray extensionVersions = new SparseIntArray(json.length()); for (int i = 0; i < json.length(); i++) { diff --git a/services/tests/servicestests/src/com/android/server/rollback/RollbackStoreTest.java b/services/tests/servicestests/src/com/android/server/rollback/RollbackStoreTest.java index 6afdfba70f58b..102d5bb373c81 100644 --- a/services/tests/servicestests/src/com/android/server/rollback/RollbackStoreTest.java +++ b/services/tests/servicestests/src/com/android/server/rollback/RollbackStoreTest.java @@ -234,8 +234,8 @@ public class RollbackStoreTest { @Test public void loadFromJsonNoExtensionVersions() throws Exception { - Rollback expectedRb = - mRollbackStore.createNonStagedRollback(ID, USER, INSTALLER, null, null); + Rollback expectedRb = mRollbackStore.createNonStagedRollback( + ID, USER, INSTALLER, null, new SparseIntArray(0)); expectedRb.setTimestamp(Instant.parse("2019-10-01T12:29:08.855Z")); expectedRb.setRestoreUserDataInProgress(true); @@ -337,7 +337,8 @@ public class RollbackStoreTest { @Test public void saveAndDelete() { - Rollback rollback = mRollbackStore.createNonStagedRollback(ID, USER, INSTALLER, null, null); + Rollback rollback = mRollbackStore.createNonStagedRollback( + ID, USER, INSTALLER, null, new SparseIntArray(0)); RollbackStore.saveRollback(rollback); diff --git a/services/tests/servicestests/src/com/android/server/rollback/RollbackUnitTest.java b/services/tests/servicestests/src/com/android/server/rollback/RollbackUnitTest.java index e74891c7b3f84..57eae1d26f7cd 100644 --- a/services/tests/servicestests/src/com/android/server/rollback/RollbackUnitTest.java +++ b/services/tests/servicestests/src/com/android/server/rollback/RollbackUnitTest.java @@ -24,12 +24,18 @@ import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import android.content.pm.PackageManagerInternal; import android.content.pm.VersionedPackage; import android.content.rollback.PackageRollbackInfo; import android.util.IntArray; +import android.util.SparseIntArray; import android.util.SparseLongArray; +import com.android.server.pm.PackageList; +import com.android.server.pm.parsing.pkg.PackageImpl; + import com.google.common.collect.Range; import org.junit.Before; @@ -44,6 +50,7 @@ import java.io.File; import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; +import java.util.List; @RunWith(JUnit4.class) public class RollbackUnitTest { @@ -56,10 +63,17 @@ public class RollbackUnitTest { private static final String INSTALLER = "some.installer"; @Mock private AppDataRollbackHelper mMockDataHelper; + @Mock private PackageManagerInternal mMockPmi; + + private List mPackages; + private PackageList mPackageList; @Before public void setUp() { MockitoAnnotations.initMocks(this); + mPackages = new ArrayList<>(); + mPackageList = new PackageList(mPackages, null); + when(mMockPmi.getPackageList()).thenReturn(mPackageList); } @Test @@ -340,6 +354,85 @@ public class RollbackUnitTest { assertThat(rollback.allPackagesEnabled()).isTrue(); } + @Test + public void minExtVerConstraintNotViolated() { + addPkgWithMinExtVersions("pkg0", new int[][] {{30, 4}}); + addPkgWithMinExtVersions("pkg1", new int[][] {}); + addPkgWithMinExtVersions("pkg2", new int[][] {{30, 5}, {31, 1}}); + addPkgWithMinExtVersions("pkg3", new int[][] {{31, 7}, {32, 15}}); + + assertThat(Rollback.extensionVersionReductionWouldViolateConstraint( + sparseArrayFrom(new int[][] {{30, 5}}), mMockPmi)).isFalse(); + } + + @Test + public void minExtVerConstraintExists() { + addPkgWithMinExtVersions("pkg0", null); + addPkgWithMinExtVersions("pkg1", new int[][] {{30, 5}, {31, 1}}); + + assertThat(Rollback.extensionVersionReductionWouldViolateConstraint( + sparseArrayFrom(new int[][] {{30, 4}}), mMockPmi)).isTrue(); + } + + @Test + public void minExtVerConstraintExistsOnOnePackage() { + addPkgWithMinExtVersions("pkg0", new int[][] {{30, 4}}); + addPkgWithMinExtVersions("pkg1", new int[][] {}); + addPkgWithMinExtVersions("pkg2", new int[][] {{30, 5}, {31, 1}}); + addPkgWithMinExtVersions("pkg3", new int[][] {{31, 7}, {32, 15}}); + + assertThat(Rollback.extensionVersionReductionWouldViolateConstraint( + sparseArrayFrom(new int[][] {{30, 4}}), mMockPmi)).isTrue(); + } + + @Test + public void minExtVerConstraintDifferentSdk() { + addPkgWithMinExtVersions("pkg0", null); + addPkgWithMinExtVersions("pkg1", new int[][] {{30, 5}, {31, 1}}); + + assertThat(Rollback.extensionVersionReductionWouldViolateConstraint( + sparseArrayFrom(new int[][] {{32, 4}}), mMockPmi)).isFalse(); + } + + @Test + public void minExtVerConstraintNoneRecordedOnRollback() { + addPkgWithMinExtVersions("pkg0", new int[][] {{30, 4}}); + addPkgWithMinExtVersions("pkg1", new int[][] {}); + addPkgWithMinExtVersions("pkg2", new int[][] {{30, 5}, {31, 1}}); + addPkgWithMinExtVersions("pkg3", new int[][] {{31, 7}, {32, 15}}); + + assertThat(Rollback.extensionVersionReductionWouldViolateConstraint( + new SparseIntArray(0), mMockPmi)).isFalse(); + } + + @Test + public void minExtVerConstraintNoMinsRecorded() { + addPkgWithMinExtVersions("pkg0", null); + addPkgWithMinExtVersions("pkg1", null); + + assertThat(Rollback.extensionVersionReductionWouldViolateConstraint( + sparseArrayFrom(new int[][] {{32, 4}}), mMockPmi)).isFalse(); + } + + private void addPkgWithMinExtVersions(String pkg, int[][] minExtVersions) { + mPackages.add(pkg); + PackageImpl pkgImpl = new PackageImpl(pkg, "baseCodePath", "codePath", null, false); + pkgImpl.setMinExtensionVersions(sparseArrayFrom(minExtVersions)); + + when(mMockPmi.getPackage(pkg)).thenReturn(pkgImpl); + } + + private static SparseIntArray sparseArrayFrom(int[][] arr) { + if (arr == null) { + return null; + } + SparseIntArray result = new SparseIntArray(arr.length); + for (int[] pair : arr) { + result.put(pair[0], pair[1]); + } + return result; + } + private static PackageRollbackInfo newPkgInfoFor( String packageName, long fromVersion, long toVersion, boolean isApex) { return new PackageRollbackInfo(new VersionedPackage(packageName, fromVersion),