diff options
| author | 2019-04-25 13:01:39 +0100 | |
|---|---|---|
| committer | 2019-05-01 09:10:36 +0000 | |
| commit | 2124d4b313e2401ab00d51192a364e7f0775cb92 (patch) | |
| tree | 76f317d6da873def8a1d652acc7296b1612878b2 | |
| parent | 7abba0a4bcb603ba5a856a4b739b59c26d73d8f8 (diff) | |
Ensure race between rollback and roll forward is properly handled
If rollback for a package is committed at the same time the package is
updated, it's possible we will incorrectly roll back the newly updated
version of the application.
Add a hidden API to the package installer that lets you set a required
existing version of a package to be updated. If the expected package
version is not installed at the time of commit, the update install
fails.
The RollbackManager uses this new API to ensure that rollback will fail
if the package in question was just updated.
Test: atest RollbackTest, with new test added and manual confirmation
that the race condition was exercised by the new test.
Bug: 128831080
Change-Id: Ifa5627e257d2ef13e2b213ef0dbc93932797ce0d
11 files changed, 202 insertions, 27 deletions
diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index d2f0fb3cc768..f1a08dfdbe87 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -1318,6 +1318,8 @@ public class PackageInstaller { public boolean isMultiPackage; /** {@hide} */ public boolean isStaged; + /** {@hide} */ + public long requiredInstalledVersionCode = PackageManager.VERSION_CODE_HIGHEST; /** * Construct parameters for a new package install session. @@ -1350,6 +1352,7 @@ public class PackageInstaller { installerPackageName = source.readString(); isMultiPackage = source.readBoolean(); isStaged = source.readBoolean(); + requiredInstalledVersionCode = source.readLong(); } /** {@hide} */ @@ -1372,6 +1375,7 @@ public class PackageInstaller { ret.installerPackageName = installerPackageName; ret.isMultiPackage = isMultiPackage; ret.isStaged = isStaged; + ret.requiredInstalledVersionCode = requiredInstalledVersionCode; return ret; } @@ -1567,6 +1571,19 @@ public class PackageInstaller { } } + /** + * Require the given version of the package be installed. + * The install will only be allowed if the existing version code of + * the package installed on the device matches the given version code. + * Use {@link * PackageManager#VERSION_CODE_HIGHEST} to allow + * installation regardless of the currently installed package version. + * + * @hide + */ + public void setRequiredInstalledVersionCode(long versionCode) { + requiredInstalledVersionCode = versionCode; + } + /** {@hide} */ public void setInstallFlagsForcePermissionPrompt() { installFlags |= PackageManager.INSTALL_FORCE_PERMISSION_PROMPT; @@ -1708,6 +1725,7 @@ public class PackageInstaller { pw.printPair("installerPackageName", installerPackageName); pw.printPair("isMultiPackage", isMultiPackage); pw.printPair("isStaged", isStaged); + pw.printPair("requiredInstalledVersionCode", requiredInstalledVersionCode); pw.println(); } @@ -1736,6 +1754,7 @@ public class PackageInstaller { dest.writeString(installerPackageName); dest.writeBoolean(isMultiPackage); dest.writeBoolean(isStaged); + dest.writeLong(requiredInstalledVersionCode); } public static final Parcelable.Creator<SessionParams> diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index a95e094ce54c..0694edeae2ed 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -1423,6 +1423,14 @@ public abstract class PackageManager { */ public static final int INSTALL_FAILED_MULTIPACKAGE_INCONSISTENCY = -120; + /** + * Installation failed return code: the required installed version code + * does not match the currently installed package version code. + * + * @hide + */ + public static final int INSTALL_FAILED_WRONG_INSTALLED_VERSION = -121; + /** @hide */ @IntDef(flag = true, prefix = { "DELETE_" }, value = { DELETE_KEEP_DATA, @@ -6926,6 +6934,7 @@ public abstract class PackageManager { case INSTALL_FAILED_BAD_DEX_METADATA: return "INSTALL_FAILED_BAD_DEX_METADATA"; case INSTALL_FAILED_MISSING_SPLIT: return "INSTALL_FAILED_MISSING_SPLIT"; case INSTALL_FAILED_BAD_SIGNATURE: return "INSTALL_FAILED_BAD_SIGNATURE"; + case INSTALL_FAILED_WRONG_INSTALLED_VERSION: return "INSTALL_FAILED_WRONG_INSTALLED_VERSION"; default: return Integer.toString(status); } } diff --git a/core/java/com/android/internal/content/PackageHelper.java b/core/java/com/android/internal/content/PackageHelper.java index 5d08a259dace..ad1ff902b7d8 100644 --- a/core/java/com/android/internal/content/PackageHelper.java +++ b/core/java/com/android/internal/content/PackageHelper.java @@ -64,6 +64,8 @@ public class PackageHelper { public static final int RECOMMEND_MEDIA_UNAVAILABLE = -5; public static final int RECOMMEND_FAILED_INVALID_URI = -6; public static final int RECOMMEND_FAILED_VERSION_DOWNGRADE = -7; + /** {@hide} */ + public static final int RECOMMEND_FAILED_WRONG_INSTALLED_VERSION = -8; private static final String TAG = "PackageHelper"; // App installation location settings values diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 20d47ed7c1a6..8621b4e9dd2b 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -14974,12 +14974,14 @@ public class PackageManagerService extends IPackageManager.Stub final int installReason; @Nullable MultiPackageInstallParams mParentInstallParams; + final long requiredInstalledVersionCode; InstallParams(OriginInfo origin, MoveInfo move, IPackageInstallObserver2 observer, int installFlags, String installerPackageName, String volumeUuid, VerificationInfo verificationInfo, UserHandle user, String packageAbiOverride, String[] grantedPermissions, List<String> whitelistedRestrictedPermissions, - PackageParser.SigningDetails signingDetails, int installReason) { + PackageParser.SigningDetails signingDetails, int installReason, + long requiredInstalledVersionCode) { super(user); this.origin = origin; this.move = move; @@ -14993,6 +14995,7 @@ public class PackageManagerService extends IPackageManager.Stub this.whitelistedRestrictedPermissions = whitelistedRestrictedPermissions; this.signingDetails = signingDetails; this.installReason = installReason; + this.requiredInstalledVersionCode = requiredInstalledVersionCode; } InstallParams(ActiveInstallSession activeInstallSession) { @@ -15023,6 +15026,8 @@ public class PackageManagerService extends IPackageManager.Stub whitelistedRestrictedPermissions = activeInstallSession.getSessionParams() .whitelistedRestrictedPermissions; signingDetails = activeInstallSession.getSigningDetails(); + requiredInstalledVersionCode = activeInstallSession.getSessionParams() + .requiredInstalledVersionCode; } @Override @@ -15051,6 +15056,23 @@ public class PackageManagerService extends IPackageManager.Stub } } + if (requiredInstalledVersionCode != PackageManager.VERSION_CODE_HIGHEST) { + if (dataOwnerPkg == null) { + Slog.w(TAG, "Required installed version code was " + + requiredInstalledVersionCode + + " but package is not installed"); + return PackageHelper.RECOMMEND_FAILED_WRONG_INSTALLED_VERSION; + } + + if (dataOwnerPkg.getLongVersionCode() != requiredInstalledVersionCode) { + Slog.w(TAG, "Required installed version code was " + + requiredInstalledVersionCode + + " but actual installed version is " + + dataOwnerPkg.getLongVersionCode()); + return PackageHelper.RECOMMEND_FAILED_WRONG_INSTALLED_VERSION; + } + } + if (dataOwnerPkg != null) { if (!PackageManagerServiceUtils.isDowngradePermitted(installFlags, dataOwnerPkg.applicationInfo.flags)) { @@ -15177,6 +15199,8 @@ public class PackageManagerService extends IPackageManager.Stub loc = installLocationPolicy(pkgLite); if (loc == PackageHelper.RECOMMEND_FAILED_VERSION_DOWNGRADE) { ret = PackageManager.INSTALL_FAILED_VERSION_DOWNGRADE; + } else if (loc == PackageHelper.RECOMMEND_FAILED_WRONG_INSTALLED_VERSION) { + ret = PackageManager.INSTALL_FAILED_WRONG_INSTALLED_VERSION; } else if (!onInt) { // Override install location with flags if (loc == PackageHelper.RECOMMEND_INSTALL_EXTERNAL) { @@ -23289,7 +23313,7 @@ public class PackageManagerService extends IPackageManager.Stub installerPackageName, volumeUuid, null /*verificationInfo*/, user, packageAbiOverride, null /*grantedPermissions*/, null /*whitelistedRestrictedPermissions*/, PackageParser.SigningDetails.UNKNOWN, - PackageManager.INSTALL_REASON_UNKNOWN); + PackageManager.INSTALL_REASON_UNKNOWN, PackageManager.VERSION_CODE_HIGHEST); params.setTraceMethod("movePackage").setTraceCookie(System.identityHashCode(params)); msg.obj = params; diff --git a/services/core/java/com/android/server/pm/StagingManager.java b/services/core/java/com/android/server/pm/StagingManager.java index 803ab2d299e2..7ba6e4a34c88 100644 --- a/services/core/java/com/android/server/pm/StagingManager.java +++ b/services/core/java/com/android/server/pm/StagingManager.java @@ -165,6 +165,22 @@ public class StagingManager { continue; } long activeVersion = activePackage.applicationInfo.longVersionCode; + if (session.params.requiredInstalledVersionCode + != PackageManager.VERSION_CODE_HIGHEST) { + if (activeVersion != session.params.requiredInstalledVersionCode) { + session.setStagedSessionFailed( + SessionInfo.STAGED_SESSION_VERIFICATION_FAILED, + "Installed version of APEX package " + newPackage.packageName + + " does not match required. Active version: " + activeVersion + + " required: " + session.params.requiredInstalledVersionCode); + + if (!mApexManager.abortActiveSession()) { + Slog.e(TAG, "Failed to abort apex session " + session.sessionId); + } + return false; + } + } + boolean allowsDowngrade = PackageManagerServiceUtils.isDowngradePermitted( session.params.installFlags, activePackage.applicationInfo.flags); if (activeVersion > newPackage.versionCode && !allowsDowngrade) { diff --git a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java index db2c742b8d4e..3a997a81a05c 100644 --- a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java +++ b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java @@ -357,31 +357,6 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub { return; } - // Verify the RollbackData is up to date with what's installed on - // device. - // TODO: We assume that between now and the time we commit the - // downgrade install, the currently installed package version does not - // change. This is not safe to assume, particularly in the case of a - // rollback racing with a roll-forward fix of a buggy package. - // Figure out how to ensure we don't commit the rollback if - // roll forward happens at the same time. - for (PackageRollbackInfo info : data.info.getPackages()) { - VersionedPackage installedVersion = getInstalledPackageVersion(info.getPackageName()); - if (installedVersion == null) { - // TODO: Test this case - sendFailure(statusReceiver, RollbackManager.STATUS_FAILURE_ROLLBACK_UNAVAILABLE, - "Package to roll back is not installed"); - return; - } - - if (!packageVersionsEqual(info.getVersionRolledBackFrom(), installedVersion)) { - // TODO: Test this case - sendFailure(statusReceiver, RollbackManager.STATUS_FAILURE_ROLLBACK_UNAVAILABLE, - "Package version to roll back not installed."); - return; - } - } - // Get a context for the caller to use to install the downgraded // version of the package. Context context = null; @@ -420,6 +395,8 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub { } } params.setRequestDowngrade(true); + params.setRequiredInstalledVersionCode( + info.getVersionRolledBackFrom().getLongVersionCode()); if (data.isStaged()) { params.setStaged(); } diff --git a/tests/RollbackTest/Android.bp b/tests/RollbackTest/Android.bp index e556b0acb1a3..1932871d86b9 100644 --- a/tests/RollbackTest/Android.bp +++ b/tests/RollbackTest/Android.bp @@ -29,6 +29,14 @@ android_test_helper_app { } android_test_helper_app { + name: "RollbackTestAppAv3", + manifest: "TestApp/Av3.xml", + sdk_version: "current", + srcs: ["TestApp/src/**/*.java"], + resource_dirs: ["TestApp/res_v3"], +} + +android_test_helper_app { name: "RollbackTestAppACrashingV2", manifest: "TestApp/ACrashingV2.xml", sdk_version: "current", @@ -118,6 +126,7 @@ android_test { java_resources: [ ":RollbackTestAppAv1", ":RollbackTestAppAv2", + ":RollbackTestAppAv3", ":RollbackTestAppACrashingV2", ":RollbackTestAppBv1", ":RollbackTestAppBv2", diff --git a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java index f9304f242ef3..28c371c034b8 100644 --- a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java +++ b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java @@ -871,6 +871,51 @@ public class RollbackTest { } } + /** + * Test race between roll back and roll forward. + */ + @Test + public void testRollForwardRace() throws Exception { + try { + RollbackTestUtils.adoptShellPermissionIdentity( + Manifest.permission.INSTALL_PACKAGES, + Manifest.permission.DELETE_PACKAGES, + Manifest.permission.TEST_MANAGE_ROLLBACKS, + Manifest.permission.MANAGE_ROLLBACKS); + + RollbackManager rm = RollbackTestUtils.getRollbackManager(); + + RollbackTestUtils.uninstall(TEST_APP_A); + RollbackTestUtils.install("RollbackTestAppAv1.apk", false); + RollbackTestUtils.install("RollbackTestAppAv2.apk", true); + assertEquals(2, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + + RollbackInfo rollback = getUniqueRollbackInfoForPackage( + rm.getAvailableRollbacks(), TEST_APP_A); + assertRollbackInfoEquals(TEST_APP_A, 2, 1, rollback); + + // Install a new version of package A, then immediately rollback + // the previous version. We expect the rollback to fail, because + // it is no longer available. + // There are a couple different ways this could fail depending on + // thread interleaving, so don't ignore flaky failures. + RollbackTestUtils.install("RollbackTestAppAv3.apk", false); + try { + RollbackTestUtils.rollback(rollback.getRollbackId()); + // Note: Don't ignore flaky failures here. + fail("Expected rollback to fail, but it did not."); + } catch (AssertionError e) { + Log.i(TAG, "Note expected failure: ", e); + // Expected + } + + // Note: Don't ignore flaky failures here. + assertEquals(3, RollbackTestUtils.getInstalledVersion(TEST_APP_A)); + } finally { + RollbackTestUtils.dropShellPermissionIdentity(); + } + } + // Helper function to test that the given rollback info is a rollback for // the atomic set {A2, B2} -> {A1, B1}. private void assertRollbackInfoForAandB(RollbackInfo rollback) { diff --git a/tests/RollbackTest/TestApp/Av3.xml b/tests/RollbackTest/TestApp/Av3.xml new file mode 100644 index 000000000000..9725c9f7cf9e --- /dev/null +++ b/tests/RollbackTest/TestApp/Av3.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.tests.rollback.testapp.A" + android:versionCode="3" + android:versionName="3.0" > + + + <uses-sdk android:minSdkVersion="19" /> + + <application android:label="Rollback Test App A v3"> + <receiver android:name="com.android.tests.rollback.testapp.ProcessUserData" + android:exported="true" /> + <activity android:name="com.android.tests.rollback.testapp.MainActivity"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + </application> +</manifest> diff --git a/tests/RollbackTest/TestApp/res_v3/values-anydpi/values.xml b/tests/RollbackTest/TestApp/res_v3/values-anydpi/values.xml new file mode 100644 index 000000000000..f2d8992bee37 --- /dev/null +++ b/tests/RollbackTest/TestApp/res_v3/values-anydpi/values.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2019 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. +--> + +<resources> + <integer name="split_version">3</integer> +</resources> diff --git a/tests/RollbackTest/TestApp/res_v3/values/values.xml b/tests/RollbackTest/TestApp/res_v3/values/values.xml new file mode 100644 index 000000000000..968168a4bf06 --- /dev/null +++ b/tests/RollbackTest/TestApp/res_v3/values/values.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2019 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. +--> + +<resources> + <integer name="app_version">3</integer> + <integer name="split_version">0</integer> +</resources> |