diff options
author | 2019-12-20 17:46:01 +0000 | |
---|---|---|
committer | 2020-01-10 16:15:47 +0000 | |
commit | 3fcecfc1ce98969c11be853a39ecd08f3ed646d7 (patch) | |
tree | 7fc6421d038e32de1589fe710db62de671644c0d | |
parent | 5ea8b0d72f47d4c2e028992294a8904a3945715e (diff) |
Rollback user data of apks-in-apex while rolling back the apex
Currently, the RollbackManager is not aware of the apk-in-apex being
installed since the install is done by PM during scan phase of boot. As
such, RM does not backup the user data of the apk-in-apex.
In the new implementation, we ask the RM to snapshot/restore user data
of apk-in-apex while resuming the apex session in StagingManager.
Bug: 142712057
Test: atest StagedRollbackTest#testRollbackApexWithApk
Test: atest AppDataRollbackHelperTest
Test: atest RollbackStoreTest
Test: atest RollbackUnitTest
Change-Id: Ibbaa5d0c98cb883588c085d77bc89c3e8217d76a
18 files changed, 500 insertions, 61 deletions
diff --git a/core/java/android/content/rollback/PackageRollbackInfo.java b/core/java/android/content/rollback/PackageRollbackInfo.java index 6378db0ebbbd..b273cd67479c 100644 --- a/core/java/android/content/rollback/PackageRollbackInfo.java +++ b/core/java/android/content/rollback/PackageRollbackInfo.java @@ -76,6 +76,11 @@ public final class PackageRollbackInfo implements Parcelable { */ private final boolean mIsApex; + /** + * Whether this instance represents the PackageRollbackInfo for an APK in APEX. + */ + private final boolean mIsApkInApex; + /* * The list of users for which snapshots have been saved. */ @@ -157,6 +162,10 @@ public final class PackageRollbackInfo implements Parcelable { public @PackageManager.RollbackDataPolicy int getRollbackDataPolicy() { return mRollbackDataPolicy; } + /** @hide */ + public boolean isApkInApex() { + return mIsApkInApex; + } /** @hide */ public IntArray getSnapshottedUsers() { @@ -190,17 +199,18 @@ public final class PackageRollbackInfo implements Parcelable { public PackageRollbackInfo(VersionedPackage packageRolledBackFrom, VersionedPackage packageRolledBackTo, @NonNull IntArray pendingBackups, @NonNull ArrayList<RestoreInfo> pendingRestores, - boolean isApex, @NonNull IntArray snapshottedUsers, + boolean isApex, boolean isApkInApex, @NonNull IntArray snapshottedUsers, @NonNull SparseLongArray ceSnapshotInodes) { this(packageRolledBackFrom, packageRolledBackTo, pendingBackups, pendingRestores, isApex, - snapshottedUsers, ceSnapshotInodes, PackageManager.RollbackDataPolicy.RESTORE); + isApkInApex, snapshottedUsers, ceSnapshotInodes, + PackageManager.RollbackDataPolicy.RESTORE); } /** @hide */ public PackageRollbackInfo(VersionedPackage packageRolledBackFrom, VersionedPackage packageRolledBackTo, @NonNull IntArray pendingBackups, @NonNull ArrayList<RestoreInfo> pendingRestores, - boolean isApex, @NonNull IntArray snapshottedUsers, + boolean isApex, boolean isApkInApex, @NonNull IntArray snapshottedUsers, @NonNull SparseLongArray ceSnapshotInodes, @PackageManager.RollbackDataPolicy int rollbackDataPolicy) { this.mVersionRolledBackFrom = packageRolledBackFrom; @@ -209,6 +219,7 @@ public final class PackageRollbackInfo implements Parcelable { this.mPendingRestores = pendingRestores; this.mIsApex = isApex; this.mRollbackDataPolicy = rollbackDataPolicy; + this.mIsApkInApex = isApkInApex; this.mSnapshottedUsers = snapshottedUsers; this.mCeSnapshotInodes = ceSnapshotInodes; } @@ -217,6 +228,7 @@ public final class PackageRollbackInfo implements Parcelable { this.mVersionRolledBackFrom = VersionedPackage.CREATOR.createFromParcel(in); this.mVersionRolledBackTo = VersionedPackage.CREATOR.createFromParcel(in); this.mIsApex = in.readBoolean(); + this.mIsApkInApex = in.readBoolean(); this.mPendingRestores = null; this.mPendingBackups = null; this.mSnapshottedUsers = null; @@ -234,6 +246,7 @@ public final class PackageRollbackInfo implements Parcelable { mVersionRolledBackFrom.writeToParcel(out, flags); mVersionRolledBackTo.writeToParcel(out, flags); out.writeBoolean(mIsApex); + out.writeBoolean(mIsApkInApex); } public static final @NonNull Parcelable.Creator<PackageRollbackInfo> CREATOR = diff --git a/services/core/java/android/content/pm/PackageManagerInternal.java b/services/core/java/android/content/pm/PackageManagerInternal.java index 312dd46fbc73..76572d35d516 100644 --- a/services/core/java/android/content/pm/PackageManagerInternal.java +++ b/services/core/java/android/content/pm/PackageManagerInternal.java @@ -811,6 +811,12 @@ public abstract class PackageManagerInternal { public abstract boolean isApexPackage(String packageName); /** + * Returns list of {@code packageName} of apks inside the given apex. + * @param apexPackageName Package name of the apk container of apex + */ + public abstract List<String> getApksInApex(String apexPackageName); + + /** * Uninstalls given {@code packageName}. * * @param packageName apex package to uninstall. diff --git a/services/core/java/com/android/server/pm/ApexManager.java b/services/core/java/com/android/server/pm/ApexManager.java index 307a07bb09a2..a009183f7ecb 100644 --- a/services/core/java/com/android/server/pm/ApexManager.java +++ b/services/core/java/com/android/server/pm/ApexManager.java @@ -32,10 +32,13 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; import android.content.pm.PackageParser; +import android.content.pm.parsing.AndroidPackage; import android.os.Environment; import android.os.RemoteException; import android.os.ServiceManager; import android.sysprop.ApexProperties; +import android.util.ArrayMap; +import android.util.ArraySet; import android.util.Singleton; import android.util.Slog; @@ -44,15 +47,20 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.BackgroundThread; import com.android.internal.util.IndentingPrintWriter; +import com.google.android.collect.Lists; + import java.io.File; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; /** @@ -97,12 +105,27 @@ abstract class ApexManager { * Minimal information about APEX mount points and the original APEX package they refer to. */ static class ActiveApexInfo { + @Nullable public final String apexModuleName; public final File apexDirectory; - public final File preinstalledApexPath; + public final File preInstalledApexPath; + + private ActiveApexInfo(File apexDirectory, File preInstalledApexPath) { + this(null, apexDirectory, preInstalledApexPath); + } - private ActiveApexInfo(File apexDirectory, File preinstalledApexPath) { + private ActiveApexInfo(@Nullable String apexModuleName, File apexDirectory, + File preInstalledApexPath) { + this.apexModuleName = apexModuleName; this.apexDirectory = apexDirectory; - this.preinstalledApexPath = preinstalledApexPath; + this.preInstalledApexPath = preInstalledApexPath; + } + + private ActiveApexInfo(ApexInfo apexInfo) { + this( + apexInfo.moduleName, + new File(Environment.getApexDirectory() + File.separator + + apexInfo.moduleName), + new File(apexInfo.preinstalledModulePath)); } } @@ -232,6 +255,17 @@ abstract class ApexManager { abstract boolean uninstallApex(String apexPackagePath); /** + * Registers an APK package as an embedded apk of apex. + */ + abstract void registerApkInApex(AndroidPackage pkg); + + /** + * Returns list of {@code packageName} of apks inside the given apex. + * @param apexPackageName Package name of the apk container of apex + */ + abstract List<String> getApksInApex(String apexPackageName); + + /** * Dumps various state information to the provided {@link PrintWriter} object. * * @param pw the {@link PrintWriter} object to send information to. @@ -255,16 +289,33 @@ abstract class ApexManager { static class ApexManagerImpl extends ApexManager { private final IApexService mApexService; private final Object mLock = new Object(); + + @GuardedBy("mLock") + private Set<ActiveApexInfo> mActiveApexInfosCache; + /** - * A map from {@code APEX packageName} to the {@Link PackageInfo} generated from the {@code - * AndroidManifest.xml} - * - * <p>Note that key of this map is {@code packageName} field of the corresponding {@code - * AndroidManifest.xml}. - */ + * Contains the list of {@code packageName}s of apks-in-apex for given + * {@code apexModuleName}. See {@link #mPackageNameToApexModuleName} to understand the + * difference between {@code packageName} and {@code apexModuleName}. + */ + @GuardedBy("mLock") + private Map<String, List<String>> mApksInApex = new ArrayMap<>(); + @GuardedBy("mLock") private List<PackageInfo> mAllPackagesCache; + /** + * An APEX is a file format that delivers the apex-payload wrapped in an apk container. The + * apk container has a reference name, called {@code packageName}, which is found inside the + * {@code AndroidManifest.xml}. The apex payload inside the container also has a reference + * name, called {@code apexModuleName}, which is found in {@code apex_manifest.json} file. + * + * {@link #mPackageNameToApexModuleName} contains the mapping from {@code packageName} of + * the apk container to {@code apexModuleName} of the apex-payload inside. + */ + @GuardedBy("mLock") + private Map<String, String> mPackageNameToApexModuleName; + ApexManagerImpl(IApexService apexService) { mApexService = apexService; } @@ -291,18 +342,25 @@ abstract class ApexManager { @Override List<ActiveApexInfo> getActiveApexInfos() { - try { - return Arrays.stream(mApexService.getActivePackages()) - .map(apexInfo -> new ActiveApexInfo( - new File( - Environment.getApexDirectory() + File.separator - + apexInfo.moduleName), - new File(apexInfo.preinstalledModulePath))).collect( - Collectors.toList()); - } catch (RemoteException e) { - Slog.e(TAG, "Unable to retrieve packages from apexservice", e); + synchronized (mLock) { + if (mActiveApexInfosCache == null) { + try { + mActiveApexInfosCache = new ArraySet<>(); + final ApexInfo[] activePackages = mApexService.getActivePackages(); + for (int i = 0; i < activePackages.length; i++) { + ApexInfo apexInfo = activePackages[i]; + mActiveApexInfosCache.add(new ActiveApexInfo(apexInfo)); + } + } catch (RemoteException e) { + Slog.e(TAG, "Unable to retrieve packages from apexservice", e); + } + } + if (mActiveApexInfosCache != null) { + return new ArrayList<>(mActiveApexInfosCache); + } else { + return Collections.emptyList(); + } } - return Collections.emptyList(); } @Override @@ -325,6 +383,7 @@ abstract class ApexManager { } try { mAllPackagesCache = new ArrayList<>(); + mPackageNameToApexModuleName = new HashMap<>(); HashSet<String> activePackagesSet = new HashSet<>(); HashSet<String> factoryPackagesSet = new HashSet<>(); final ApexInfo[] allPkgs = mApexService.getAllPackages(); @@ -350,6 +409,7 @@ abstract class ApexManager { final PackageInfo packageInfo = PackageParser.generatePackageInfo(pkg, ai, flags); mAllPackagesCache.add(packageInfo); + mPackageNameToApexModuleName.put(packageInfo.packageName, ai.moduleName); if (ai.isActive) { if (activePackagesSet.contains(packageInfo.packageName)) { throw new IllegalStateException( @@ -366,7 +426,6 @@ abstract class ApexManager { } factoryPackagesSet.add(packageInfo.packageName); } - } } catch (RemoteException re) { Slog.e(TAG, "Unable to retrieve packages from apexservice: " + re.toString()); @@ -533,6 +592,37 @@ abstract class ApexManager { } } + @Override + void registerApkInApex(AndroidPackage pkg) { + synchronized (mLock) { + final Iterator<ActiveApexInfo> it = mActiveApexInfosCache.iterator(); + while (it.hasNext()) { + final ActiveApexInfo aai = it.next(); + if (pkg.getBaseCodePath().startsWith(aai.apexDirectory.getAbsolutePath())) { + List<String> apks = mApksInApex.get(aai.apexModuleName); + if (apks == null) { + apks = Lists.newArrayList(); + mApksInApex.put(aai.apexModuleName, apks); + } + apks.add(pkg.getPackageName()); + } + } + } + } + + @Override + List<String> getApksInApex(String apexPackageName) { + // TODO(b/142712057): Avoid calling populateAllPackagesCacheIfNeeded during boot. + populateAllPackagesCacheIfNeeded(); + synchronized (mLock) { + String moduleName = mPackageNameToApexModuleName.get(apexPackageName); + if (moduleName == null) { + return Collections.emptyList(); + } + return mApksInApex.getOrDefault(moduleName, Collections.emptyList()); + } + } + /** * Dump information about the packages contained in a particular cache * @param packagesCache the cache to print information about. @@ -614,7 +704,6 @@ abstract class ApexManager { * updating APEX packages. */ private static final class ApexManagerFlattenedApex extends ApexManager { - @Override List<ActiveApexInfo> getActiveApexInfos() { // There is no apexd running in case of flattened apex @@ -721,6 +810,16 @@ abstract class ApexManager { } @Override + void registerApkInApex(AndroidPackage pkg) { + // No-op + } + + @Override + List<String> getApksInApex(String apexPackageName) { + return Collections.emptyList(); + } + + @Override void dump(PrintWriter pw, String packageName) { // No-op } diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index c43f23454ca6..6331dd46c035 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -188,7 +188,7 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements } }; - public PackageInstallerService(Context context, PackageManagerService pm, ApexManager am) { + public PackageInstallerService(Context context, PackageManagerService pm) { mContext = context; mPm = pm; mPermissionManager = LocalServices.getService(PermissionManagerServiceInternal.class); @@ -206,9 +206,8 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements mSessionsDir = new File(Environment.getDataSystemDirectory(), "install_sessions"); mSessionsDir.mkdirs(); - mApexManager = am; - - mStagingManager = new StagingManager(this, am, context); + mApexManager = ApexManager.getInstance(); + mStagingManager = new StagingManager(this, context); } boolean okToSendBroadcasts() { diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index a9571d97200c..52e8e37cc3f9 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -492,6 +492,7 @@ public class PackageManagerService extends IPackageManager.Stub static final int SCAN_AS_PRODUCT = 1 << 20; static final int SCAN_AS_SYSTEM_EXT = 1 << 21; static final int SCAN_AS_ODM = 1 << 22; + static final int SCAN_AS_APK_IN_APEX = 1 << 23; @IntDef(flag = true, prefix = { "SCAN_" }, value = { SCAN_NO_DEX, @@ -2589,6 +2590,9 @@ public class PackageManagerService extends IPackageManager.Stub & (SCAN_AS_VENDOR | SCAN_AS_ODM | SCAN_AS_PRODUCT | SCAN_AS_SYSTEM_EXT)) != 0) { return true; } + if ((scanFlags & SCAN_AS_APK_IN_APEX) != 0) { + return true; + } return false; } @@ -3332,7 +3336,7 @@ public class PackageManagerService extends IPackageManager.Stub } } - mInstallerService = new PackageInstallerService(mContext, this, mApexManager); + mInstallerService = new PackageInstallerService(mContext, this); final Pair<ComponentName, String> instantAppResolverComponent = getInstantAppResolverLPr(); if (instantAppResolverComponent != null) { @@ -11710,6 +11714,9 @@ public class PackageManagerService extends IPackageManager.Stub mSettings.insertPackageSettingLPw(pkgSetting, pkg); // Add the new setting to mPackages mPackages.put(pkg.getAppInfoPackageName(), pkg); + if ((scanFlags & SCAN_AS_APK_IN_APEX) != 0) { + mApexManager.registerApkInApex(pkg); + } // Add the package's KeySets to the global KeySetManagerService KeySetManagerService ksms = mSettings.mKeySetManagerService; @@ -17757,10 +17764,10 @@ public class PackageManagerService extends IPackageManager.Stub ApexManager.ActiveApexInfo apexInfo) { for (int i = 0, size = SYSTEM_PARTITIONS.size(); i < size; i++) { SystemPartition sp = SYSTEM_PARTITIONS.get(i); - if (apexInfo.preinstalledApexPath.getAbsolutePath().startsWith( + if (apexInfo.preInstalledApexPath.getAbsolutePath().startsWith( sp.folder.getAbsolutePath())) { - return new SystemPartition(apexInfo.apexDirectory, sp.scanFlag, - false /* hasOverlays */); + return new SystemPartition(apexInfo.apexDirectory, + sp.scanFlag | SCAN_AS_APK_IN_APEX, false /* hasOverlays */); } } return null; @@ -23452,6 +23459,11 @@ public class PackageManagerService extends IPackageManager.Stub } @Override + public List<String> getApksInApex(String apexPackageName) { + return PackageManagerService.this.mApexManager.getApksInApex(apexPackageName); + } + + @Override public void uninstallApex(String packageName, long versionCode, int userId, IntentSender intentSender, int flags) { final int callerUid = Binder.getCallingUid(); diff --git a/services/core/java/com/android/server/pm/StagingManager.java b/services/core/java/com/android/server/pm/StagingManager.java index 9e462cd529bb..2265d010216e 100644 --- a/services/core/java/com/android/server/pm/StagingManager.java +++ b/services/core/java/com/android/server/pm/StagingManager.java @@ -33,11 +33,13 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageInstaller; import android.content.pm.PackageInstaller.SessionInfo; import android.content.pm.PackageManager; +import android.content.pm.PackageManagerInternal; import android.content.pm.PackageParser; import android.content.pm.PackageParser.PackageParserException; import android.content.pm.PackageParser.SigningDetails; import android.content.pm.PackageParser.SigningDetails.SignatureSchemeVersion; import android.content.pm.ParceledListSlice; +import android.content.pm.parsing.AndroidPackage; import android.content.rollback.IRollbackManager; import android.content.rollback.RollbackInfo; import android.content.rollback.RollbackManager; @@ -50,6 +52,8 @@ import android.os.ParcelFileDescriptor; import android.os.PowerManager; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.UserHandle; +import android.os.UserManagerInternal; import android.os.storage.IStorageManager; import android.os.storage.StorageManager; import android.util.IntArray; @@ -61,6 +65,7 @@ import android.util.apk.ApkSignatureVerifier; import com.android.internal.annotations.GuardedBy; import com.android.internal.content.PackageHelper; import com.android.internal.os.BackgroundThread; +import com.android.server.LocalServices; import java.io.File; import java.io.IOException; @@ -93,10 +98,11 @@ public class StagingManager { @GuardedBy("mStagedSessions") private final SparseIntArray mSessionRollbackIds = new SparseIntArray(); - StagingManager(PackageInstallerService pi, ApexManager am, Context context) { + StagingManager(PackageInstallerService pi, Context context) { mPi = pi; - mApexManager = am; mContext = context; + + mApexManager = ApexManager.getInstance(); mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); mPreRebootVerificationHandler = new PreRebootVerificationHandler( BackgroundThread.get().getLooper()); @@ -334,6 +340,88 @@ public class StagingManager { return PackageHelper.getStorageManager().needsCheckpoint(); } + /** + * Apks inside apex are not installed using apk-install flow. They are scanned from the system + * directory directly by PackageManager, as such, RollbackManager need to handle their data + * separately here. + */ + private void snapshotAndRestoreApkInApexUserData(PackageInstallerSession session) { + // We want to process apks inside apex. So current session needs to contain apex. + if (!sessionContainsApex(session)) { + return; + } + + boolean doSnapshotOrRestore = + (session.params.installFlags & PackageManager.INSTALL_ENABLE_ROLLBACK) != 0 + || session.params.installReason == PackageManager.INSTALL_REASON_ROLLBACK; + if (!doSnapshotOrRestore) { + return; + } + + // Find all the apex sessions that needs processing + List<PackageInstallerSession> apexSessions = new ArrayList<>(); + if (session.isMultiPackage()) { + List<PackageInstallerSession> childrenSessions = new ArrayList<>(); + synchronized (mStagedSessions) { + for (int childSessionId : session.getChildSessionIds()) { + PackageInstallerSession childSession = mStagedSessions.get(childSessionId); + if (childSession != null) { + childrenSessions.add(childSession); + } + } + } + for (PackageInstallerSession childSession : childrenSessions) { + if (sessionContainsApex(childSession)) { + apexSessions.add(childSession); + } + } + } else { + apexSessions.add(session); + } + + // For each apex, process the apks inside it + for (PackageInstallerSession apexSession : apexSessions) { + List<String> apksInApex = mApexManager.getApksInApex(apexSession.getPackageName()); + for (String apk: apksInApex) { + snapshotAndRestoreApkInApexUserData(apk); + } + } + } + + private void snapshotAndRestoreApkInApexUserData(String packageName) { + IRollbackManager rm = IRollbackManager.Stub.asInterface( + ServiceManager.getService(Context.ROLLBACK_SERVICE)); + + PackageManagerInternal mPmi = LocalServices.getService(PackageManagerInternal.class); + AndroidPackage pkg = mPmi.getPackage(packageName); + if (pkg == null) { + Slog.e(TAG, "Could not find package: " + packageName + + "for snapshotting/restoring user data."); + return; + } + final String seInfo = pkg.getSeInfo(); + final UserManagerInternal um = LocalServices.getService(UserManagerInternal.class); + final int[] allUsers = um.getUserIds(); + + int appId = -1; + long ceDataInode = -1; + final PackageSetting ps = (PackageSetting) mPmi.getPackageSetting(packageName); + if (ps != null && rm != null) { + appId = ps.appId; + ceDataInode = ps.getCeDataInode(UserHandle.USER_SYSTEM); + // NOTE: We ignore the user specified in the InstallParam because we know this is + // an update, and hence need to restore data for all installed users. + final int[] installedUsers = ps.queryInstalledUsers(allUsers, true); + + try { + rm.snapshotAndRestoreUserData(packageName, installedUsers, appId, ceDataInode, + seInfo, 0 /*token*/); + } catch (RemoteException re) { + Slog.e(TAG, "Error snapshotting/restoring user data: " + re); + } + } + } + private void resumeSession(@NonNull PackageInstallerSession session) { Slog.d(TAG, "Resuming session " + session.sessionId); @@ -407,6 +495,7 @@ public class StagingManager { abortCheckpoint(); return; } + snapshotAndRestoreApkInApexUserData(session); Slog.i(TAG, "APEX packages in session " + session.sessionId + " were successfully activated. Proceeding with APK packages, if any"); } @@ -529,7 +618,7 @@ public class StagingManager { Arrays.stream(session.getChildSessionIds()) // Retrieve cached sessions matching ids. .mapToObj(i -> mStagedSessions.get(i)) - // Filter only the ones containing APKs.s + // Filter only the ones containing APKs. .filter(childSession -> !isApexSession(childSession)) .collect(Collectors.toList()); } diff --git a/services/core/java/com/android/server/rollback/Rollback.java b/services/core/java/com/android/server/rollback/Rollback.java index 88c1564fdb60..d719afb70796 100644 --- a/services/core/java/com/android/server/rollback/Rollback.java +++ b/services/core/java/com/android/server/rollback/Rollback.java @@ -323,8 +323,8 @@ class Rollback { new VersionedPackage(packageName, newVersion), new VersionedPackage(packageName, installedVersion), new IntArray() /* pendingBackups */, new ArrayList<>() /* pendingRestores */, - isApex, new IntArray(), new SparseLongArray() /* ceSnapshotInodes */, - rollbackDataPolicy); + isApex, false /* isApkInApex */, new IntArray(), + new SparseLongArray() /* ceSnapshotInodes */, rollbackDataPolicy); synchronized (mLock) { info.getPackages().add(packageRollbackInfo); @@ -334,6 +334,30 @@ class Rollback { } /** + * Enables this rollback for the provided apk-in-apex. + * + * @return boolean True if the rollback was enabled successfully for the specified package. + */ + boolean enableForPackageInApex(String packageName, long installedVersion, + int rollbackDataPolicy) { + // TODO(b/142712057): Extract the new version number of apk-in-apex + // The new version for the apk-in-apex is set to 0 for now. If the package is then further + // updated via non-staged install flow, then RollbackManagerServiceImpl#onPackageReplaced() + // will be called and this rollback will be deleted. Other ways of package update have not + // been handled yet. + PackageRollbackInfo packageRollbackInfo = new PackageRollbackInfo( + new VersionedPackage(packageName, 0 /* newVersion */), + new VersionedPackage(packageName, installedVersion), + new IntArray() /* pendingBackups */, new ArrayList<>() /* pendingRestores */, + false /* isApex */, true /* isApkInApex */, new IntArray(), + new SparseLongArray() /* ceSnapshotInodes */, rollbackDataPolicy); + synchronized (mLock) { + info.getPackages().add(packageRollbackInfo); + } + return true; + } + + /** * Snapshots user data for the provided package and user ids. Does nothing if this rollback is * not in the ENABLING state. */ @@ -428,6 +452,11 @@ class Rollback { parentSessionId); for (PackageRollbackInfo pkgRollbackInfo : info.getPackages()) { + if (pkgRollbackInfo.isApkInApex()) { + // No need to issue a downgrade install request for apk-in-apex. It will + // be rolled back when its parent apex is downgraded. + continue; + } PackageInstaller.SessionParams params = new PackageInstaller.SessionParams( PackageInstaller.SessionParams.MODE_FULL_INSTALL); String installerPackageName = mInstallerPackageName; @@ -453,7 +482,8 @@ class Rollback { this, pkgRollbackInfo.getPackageName()); if (packageCodePaths == null) { sendFailure(context, statusReceiver, RollbackManager.STATUS_FAILURE, - "Backup copy of package inaccessible"); + "Backup copy of package: " + + pkgRollbackInfo.getPackageName() + " is inaccessible"); return; } diff --git a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java index e29d1a765d69..2a59af3bee61 100644 --- a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java +++ b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java @@ -891,9 +891,36 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub { } ApplicationInfo appInfo = pkgInfo.applicationInfo; - return rollback.enableForPackage(packageName, newPackage.versionCode, + boolean success = rollback.enableForPackage(packageName, newPackage.versionCode, pkgInfo.getLongVersionCode(), isApex, appInfo.sourceDir, appInfo.splitSourceDirs, session.rollbackDataPolicy); + if (!success) { + return success; + } + + if (isApex) { + // Check if this apex contains apks inside it. If true, then they should be added as + // a RollbackPackageInfo into this rollback + final PackageManagerInternal pmi = LocalServices.getService( + PackageManagerInternal.class); + List<String> apksInApex = pmi.getApksInApex(packageName); + for (String apkInApex : apksInApex) { + // Get information about the currently installed package. + final PackageInfo apkPkgInfo; + try { + apkPkgInfo = getPackageInfo(apkInApex); + } catch (PackageManager.NameNotFoundException e) { + // TODO: Support rolling back fresh package installs rather than + // fail here. Test this case. + Slog.e(TAG, apkInApex + " is not installed"); + return false; + } + success = rollback.enableForPackageInApex( + apkInApex, apkPkgInfo.getLongVersionCode(), session.rollbackDataPolicy); + if (!success) return success; + } + } + return true; } @Override @@ -907,9 +934,13 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub { getHandler().post(() -> { snapshotUserDataInternal(packageName, userIds); restoreUserDataInternal(packageName, userIds, appId, seInfo); - final PackageManagerInternal pmi = LocalServices.getService( - PackageManagerInternal.class); - pmi.finishPackageInstall(token, false); + // When this method is called as part of the install flow, a positive token number is + // passed to it. Need to notify the PackageManager when we are done. + if (token > 0) { + final PackageManagerInternal pmi = LocalServices.getService( + PackageManagerInternal.class); + pmi.finishPackageInstall(token, false); + } }); } @@ -1195,11 +1226,12 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub { return null; } - if (rollback.getPackageCount() != newRollback.getPackageSessionIdCount()) { - Slog.e(TAG, "Failed to enable rollback for all packages in session."); - rollback.delete(mAppDataRollbackHelper); - return null; - } + // TODO(b/142712057): Re-enable this check. For that we need count of apks-in-apex +// if (rollback.getPackageCount() != newRollback.getPackageSessionIdCount()) { +// Slog.e(TAG, "Failed to enable rollback for all packages in session."); +// rollback.delete(mAppDataRollbackHelper); +// return null; +// } rollback.saveRollback(); synchronized (mLock) { diff --git a/services/core/java/com/android/server/rollback/RollbackStore.java b/services/core/java/com/android/server/rollback/RollbackStore.java index df75a29edd79..bbcd0def05a8 100644 --- a/services/core/java/com/android/server/rollback/RollbackStore.java +++ b/services/core/java/com/android/server/rollback/RollbackStore.java @@ -341,6 +341,7 @@ class RollbackStore { json.put("pendingRestores", convertToJsonArray(pendingRestores)); json.put("isApex", info.isApex()); + json.put("isApkInApex", info.isApkInApex()); // Field is named 'installedUsers' for legacy reasons. json.put("installedUsers", convertToJsonArray(snapshottedUsers)); @@ -364,6 +365,7 @@ class RollbackStore { json.getJSONArray("pendingRestores")); final boolean isApex = json.getBoolean("isApex"); + final boolean isApkInApex = json.getBoolean("isApkInApex"); // Field is named 'installedUsers' for legacy reasons. final IntArray snapshottedUsers = convertToIntArray(json.getJSONArray("installedUsers")); @@ -375,8 +377,8 @@ class RollbackStore { PackageManager.RollbackDataPolicy.RESTORE); return new PackageRollbackInfo(versionRolledBackFrom, versionRolledBackTo, - pendingBackups, pendingRestores, isApex, snapshottedUsers, ceSnapshotInodes, - rollbackDataPolicy); + pendingBackups, pendingRestores, isApex, isApkInApex, snapshottedUsers, + ceSnapshotInodes, rollbackDataPolicy); } private static JSONArray versionedPackagesToJson(List<VersionedPackage> packages) diff --git a/services/tests/servicestests/src/com/android/server/rollback/AppDataRollbackHelperTest.java b/services/tests/servicestests/src/com/android/server/rollback/AppDataRollbackHelperTest.java index a83d94001cf8..f871203728c0 100644 --- a/services/tests/servicestests/src/com/android/server/rollback/AppDataRollbackHelperTest.java +++ b/services/tests/servicestests/src/com/android/server/rollback/AppDataRollbackHelperTest.java @@ -98,7 +98,7 @@ public class AppDataRollbackHelperTest { final int[] installedUsers) { return new PackageRollbackInfo( new VersionedPackage(packageName, 2), new VersionedPackage(packageName, 1), - new IntArray(), new ArrayList<>(), false, IntArray.wrap(installedUsers), + new IntArray(), new ArrayList<>(), false, false, IntArray.wrap(installedUsers), new SparseLongArray()); } 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 757a884f8ded..d0d2edc59861 100644 --- a/services/tests/servicestests/src/com/android/server/rollback/RollbackStoreTest.java +++ b/services/tests/servicestests/src/com/android/server/rollback/RollbackStoreTest.java @@ -87,13 +87,15 @@ public class RollbackStoreTest { + "[{'versionRolledBackFrom':{'packageName':'blah','longVersionCode':55}," + "'versionRolledBackTo':{'packageName':'blah1','longVersionCode':50},'pendingBackups':" + "[59,1245,124544],'pendingRestores':[{'userId':498,'appId':32322,'seInfo':'wombles'}," - + "{'userId':-895,'appId':1,'seInfo':'pingu'}],'isApex':false,'installedUsers':" + + "{'userId':-895,'appId':1,'seInfo':'pingu'}],'isApex':false,'isApkInApex':false," + + "'installedUsers':" + "[498468432,1111,98464],'ceSnapshotInodes':[{'userId':1,'ceSnapshotInode':-6}," + "{'userId':2222,'ceSnapshotInode':81641654445},{'userId':546546," + "'ceSnapshotInode':345689375}]},{'versionRolledBackFrom':{'packageName':'chips'," + "'longVersionCode':28},'versionRolledBackTo':{'packageName':'com.chips.test'," + "'longVersionCode':48},'pendingBackups':[5],'pendingRestores':[{'userId':18," - + "'appId':-12,'seInfo':''}],'isApex':false,'installedUsers':[55,79]," + + "'appId':-12,'seInfo':''}],'isApex':false,'isApkInApex':false," + + "'installedUsers':[55,79]," + "'ceSnapshotInodes':[]}],'isStaged':false,'causePackages':[{'packageName':'hello'," + "'longVersionCode':23},{'packageName':'something','longVersionCode':999}]," + "'committedSessionId':45654465},'timestamp':'2019-10-01T12:29:08.855Z'," @@ -155,7 +157,7 @@ public class RollbackStoreTest { PackageRollbackInfo pkgInfo1 = new PackageRollbackInfo(new VersionedPackage("com.made.up", 18), new VersionedPackage("com.something.else", 5), new IntArray(), - new ArrayList<>(), false, new IntArray(), new SparseLongArray()); + new ArrayList<>(), false, false, new IntArray(), new SparseLongArray()); pkgInfo1.getPendingBackups().add(8); pkgInfo1.getPendingBackups().add(888); pkgInfo1.getPendingBackups().add(88885); @@ -175,7 +177,7 @@ public class RollbackStoreTest { PackageRollbackInfo pkgInfo2 = new PackageRollbackInfo( new VersionedPackage("another.package", 2), new VersionedPackage("com.test.ing", 48888), new IntArray(), new ArrayList<>(), - false, new IntArray(), new SparseLongArray()); + false, false, new IntArray(), new SparseLongArray()); pkgInfo2.getPendingBackups().add(57); pkgInfo2.getPendingRestores().add( @@ -205,7 +207,7 @@ public class RollbackStoreTest { PackageRollbackInfo pkgInfo1 = new PackageRollbackInfo(new VersionedPackage("blah", 55), new VersionedPackage("blah1", 50), new IntArray(), new ArrayList<>(), - false, new IntArray(), new SparseLongArray()); + false, false, new IntArray(), new SparseLongArray()); pkgInfo1.getPendingBackups().add(59); pkgInfo1.getPendingBackups().add(1245); pkgInfo1.getPendingBackups().add(124544); @@ -224,7 +226,7 @@ public class RollbackStoreTest { PackageRollbackInfo pkgInfo2 = new PackageRollbackInfo(new VersionedPackage("chips", 28), new VersionedPackage("com.chips.test", 48), new IntArray(), new ArrayList<>(), - false, new IntArray(), new SparseLongArray()); + false, false, new IntArray(), new SparseLongArray()); pkgInfo2.getPendingBackups().add(5); pkgInfo2.getPendingRestores().add( 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 e368d634b968..164c88382828 100644 --- a/services/tests/servicestests/src/com/android/server/rollback/RollbackUnitTest.java +++ b/services/tests/servicestests/src/com/android/server/rollback/RollbackUnitTest.java @@ -295,7 +295,8 @@ public class RollbackUnitTest { String packageName, long fromVersion, long toVersion, boolean isApex) { return new PackageRollbackInfo(new VersionedPackage(packageName, fromVersion), new VersionedPackage(packageName, toVersion), - new IntArray(), new ArrayList<>(), isApex, new IntArray(), new SparseLongArray()); + new IntArray(), new ArrayList<>(), isApex, false, new IntArray(), + new SparseLongArray()); } private static class PackageRollbackInfoForPackage implements diff --git a/tests/RollbackTest/Android.bp b/tests/RollbackTest/Android.bp index 2bc129ae4840..091edd4dc0d9 100644 --- a/tests/RollbackTest/Android.bp +++ b/tests/RollbackTest/Android.bp @@ -19,15 +19,17 @@ android_test { static_libs: ["androidx.test.rules", "cts-rollback-lib", "cts-install-lib"], test_suites: ["general-tests"], test_config: "RollbackTest.xml", + java_resources: [":com.android.apex.apkrollback.test_v2"], } java_test_host { name: "StagedRollbackTest", srcs: ["StagedRollbackTest/src/**/*.java"], libs: ["tradefed"], - static_libs: ["testng"], + static_libs: ["testng", "compatibility-tradefed"], test_suites: ["general-tests"], test_config: "StagedRollbackTest.xml", + data: [":com.android.apex.apkrollback.test_v1"], } java_test_host { @@ -37,3 +39,44 @@ java_test_host { test_suites: ["general-tests"], test_config: "MultiUserRollbackTest.xml", } + +genrule { + name: "com.android.apex.apkrollback.test.pem", + out: ["com.android.apex.apkrollback.test.pem"], + cmd: "openssl genrsa -out $(out) 4096", +} + +genrule { + name: "com.android.apex.apkrollback.test.pubkey", + srcs: [":com.android.apex.apkrollback.test.pem"], + out: ["com.android.apex.apkrollback.test.pubkey"], + tools: ["avbtool"], + cmd: "$(location avbtool) extract_public_key --key $(in) --output $(out)", +} + +apex_key { + name: "com.android.apex.apkrollback.test.key", + private_key: ":com.android.apex.apkrollback.test.pem", + public_key: ":com.android.apex.apkrollback.test.pubkey", + installable: false, +} + +apex { + name: "com.android.apex.apkrollback.test_v1", + manifest: "testdata/manifest_v1.json", + androidManifest: "testdata/AndroidManifest.xml", + file_contexts: ":apex.test-file_contexts", + key: "com.android.apex.apkrollback.test.key", + apps: ["TestAppAv1"], + installable: false, +} + +apex { + name: "com.android.apex.apkrollback.test_v2", + manifest: "testdata/manifest_v2.json", + androidManifest: "testdata/AndroidManifest.xml", + file_contexts: ":apex.test-file_contexts", + key: "com.android.apex.apkrollback.test.key", + apps: ["TestAppAv2"], + installable: false, +}
\ No newline at end of file diff --git a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java index 9e490f765eab..3877cc139a3e 100644 --- a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java +++ b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java @@ -435,11 +435,64 @@ public class StagedRollbackTest { // testNativeWatchdogTriggersRollback will fail if multiple staged sessions are // committed on a device which doesn't support checkpoint. Let's clean up all rollbacks // so there is only one rollback to commit when testing native crashes. - RollbackManager rm = RollbackUtils.getRollbackManager(); + RollbackManager rm = RollbackUtils.getRollbackManager(); rm.getAvailableRollbacks().stream().flatMap(info -> info.getPackages().stream()) .map(info -> info.getPackageName()).forEach(rm::expireRollbackForPackage); } + private static final String APK_IN_APEX_TESTAPEX_NAME = "com.android.apex.apkrollback.test"; + private static final TestApp TEST_APEX_WITH_APK_V1 = new TestApp("TestApexWithApkV1", + APK_IN_APEX_TESTAPEX_NAME, 1, /*isApex*/true, APK_IN_APEX_TESTAPEX_NAME + "_v1.apex"); + private static final TestApp TEST_APEX_WITH_APK_V2 = new TestApp("TestApexWithApkV2", + APK_IN_APEX_TESTAPEX_NAME, 2, /*isApex*/true, APK_IN_APEX_TESTAPEX_NAME + "_v2.apex"); + private static final TestApp TEST_APP_A_V2_UNKNOWN = new TestApp("Av2Unknown", TestApp.A, 0, + /*isApex*/false, "TestAppAv2.apk"); + + @Test + public void testRollbackApexWithApk_Phase1() throws Exception { + assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1); + InstallUtils.processUserData(TestApp.A); + + int sessionId = Install.single(TEST_APEX_WITH_APK_V2).setStaged().setEnableRollback() + .commit(); + InstallUtils.waitForSessionReady(sessionId); + } + + @Test + public void testRollbackApexWithApk_Phase2() throws Exception { + assertThat(InstallUtils.getInstalledVersion(APK_IN_APEX_TESTAPEX_NAME)).isEqualTo(2); + assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2); + InstallUtils.processUserData(TestApp.A); + + RollbackInfo available = RollbackUtils.getAvailableRollback(APK_IN_APEX_TESTAPEX_NAME); + assertThat(available).isStaged(); + assertThat(available).packagesContainsExactly( + Rollback.from(TEST_APEX_WITH_APK_V2).to(TEST_APEX_WITH_APK_V1), + Rollback.from(TEST_APP_A_V2_UNKNOWN).to(TestApp.A1)); + + RollbackUtils.rollback(available.getRollbackId(), TEST_APEX_WITH_APK_V2); + RollbackInfo committed = RollbackUtils.getCommittedRollbackById(available.getRollbackId()); + assertThat(committed).isNotNull(); + assertThat(committed).isStaged(); + assertThat(committed).packagesContainsExactly( + Rollback.from(TEST_APEX_WITH_APK_V2).to(TEST_APEX_WITH_APK_V1), + Rollback.from(TEST_APP_A_V2_UNKNOWN).to(TestApp.A1)); + assertThat(committed).causePackagesContainsExactly(TEST_APEX_WITH_APK_V2); + assertThat(committed.getCommittedSessionId()).isNotEqualTo(-1); + + // Note: The app is not rolled back until after the rollback is staged + // and the device has been rebooted. + InstallUtils.waitForSessionReady(committed.getCommittedSessionId()); + assertThat(InstallUtils.getInstalledVersion(APK_IN_APEX_TESTAPEX_NAME)).isEqualTo(2); + } + + @Test + public void testRollbackApexWithApk_Phase3() throws Exception { + assertThat(InstallUtils.getInstalledVersion(APK_IN_APEX_TESTAPEX_NAME)).isEqualTo(1); + assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1); + InstallUtils.processUserData(TestApp.A); + } + private static void runShellCommand(String cmd) { ParcelFileDescriptor pfd = InstrumentationRegistry.getInstrumentation().getUiAutomation() .executeShellCommand(cmd); diff --git a/tests/RollbackTest/StagedRollbackTest/src/com/android/tests/rollback/host/StagedRollbackTest.java b/tests/RollbackTest/StagedRollbackTest/src/com/android/tests/rollback/host/StagedRollbackTest.java index 91577c202df9..6daa6bc723c4 100644 --- a/tests/RollbackTest/StagedRollbackTest/src/com/android/tests/rollback/host/StagedRollbackTest.java +++ b/tests/RollbackTest/StagedRollbackTest/src/com/android/tests/rollback/host/StagedRollbackTest.java @@ -19,6 +19,7 @@ package com.android.tests.rollback.host; import static org.junit.Assert.assertTrue; import static org.testng.Assert.assertThrows; +import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper; import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; @@ -27,6 +28,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.io.File; import java.util.concurrent.TimeUnit; /** @@ -48,14 +50,32 @@ public class StagedRollbackTest extends BaseHostJUnit4Test { phase)); } + private static final String APK_IN_APEX_TESTAPEX_NAME = "com.android.apex.apkrollback.test"; + @Before public void setUp() throws Exception { + if (!getDevice().isAdbRoot()) { + getDevice().enableAdbRoot(); + } + getDevice().remountSystemWritable(); + getDevice().executeShellCommand( + "rm -f /system/apex/" + APK_IN_APEX_TESTAPEX_NAME + "*.apex " + + "/data/apex/active/" + APK_IN_APEX_TESTAPEX_NAME + "*.apex"); getDevice().reboot(); } @After public void tearDown() throws Exception { runPhase("testCleanUp"); + + if (!getDevice().isAdbRoot()) { + getDevice().enableAdbRoot(); + } + getDevice().remountSystemWritable(); + getDevice().executeShellCommand( + "rm -f /system/apex/" + APK_IN_APEX_TESTAPEX_NAME + "*.apex " + + "/data/apex/active/" + APK_IN_APEX_TESTAPEX_NAME + "*.apex"); + getDevice().reboot(); } /** @@ -184,6 +204,28 @@ public class StagedRollbackTest extends BaseHostJUnit4Test { runPhase("testRollbackDataPolicy_Phase3"); } + /** + * Tests that userdata of apk-in-apex is restored when apex is rolled back. + */ + @Test + public void testRollbackApexWithApk() throws Exception { + getDevice().uninstallPackage("com.android.cts.install.lib.testapp.A"); + CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(getBuild()); + final String fileName = APK_IN_APEX_TESTAPEX_NAME + "_v1.apex"; + final File apex = buildHelper.getTestFile(fileName); + if (!getDevice().isAdbRoot()) { + getDevice().enableAdbRoot(); + } + getDevice().remountSystemWritable(); + assertTrue(getDevice().pushFile(apex, "/system/apex/" + fileName)); + getDevice().reboot(); + runPhase("testRollbackApexWithApk_Phase1"); + getDevice().reboot(); + runPhase("testRollbackApexWithApk_Phase2"); + getDevice().reboot(); + runPhase("testRollbackApexWithApk_Phase3"); + } + private void crashProcess(String processName, int numberOfCrashes) throws Exception { String pid = ""; String lastPid = "invalid"; diff --git a/tests/RollbackTest/testdata/AndroidManifest.xml b/tests/RollbackTest/testdata/AndroidManifest.xml new file mode 100644 index 000000000000..f21ec899eb69 --- /dev/null +++ b/tests/RollbackTest/testdata/AndroidManifest.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.apex.apkrollback.test"> + <!-- APEX does not have classes.dex --> + <application android:hasCode="false" /> + <uses-sdk android:minSdkVersion="28" android:targetSdkVersion="29"/> +</manifest> + diff --git a/tests/RollbackTest/testdata/manifest_v1.json b/tests/RollbackTest/testdata/manifest_v1.json new file mode 100644 index 000000000000..1762fc6764cf --- /dev/null +++ b/tests/RollbackTest/testdata/manifest_v1.json @@ -0,0 +1,4 @@ +{ + "name": "com.android.apex.apkrollback.test", + "version": 1 +} diff --git a/tests/RollbackTest/testdata/manifest_v2.json b/tests/RollbackTest/testdata/manifest_v2.json new file mode 100644 index 000000000000..c5127b9c3023 --- /dev/null +++ b/tests/RollbackTest/testdata/manifest_v2.json @@ -0,0 +1,4 @@ +{ + "name": "com.android.apex.apkrollback.test", + "version": 2 +} |