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

Commit a3bf162a authored by Narayan Kamath's avatar Narayan Kamath Committed by Android (Google) Code Review
Browse files

Merge "Multi-user support for app data snapshot / rollback."

parents aab0a275 c034fe9e
Loading
Loading
Loading
Loading
+68 −1
Original line number Diff line number Diff line
@@ -16,10 +16,14 @@

package android.content.rollback;

import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.content.pm.VersionedPackage;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.IntArray;

import java.util.ArrayList;

/**
 * Information about a rollback available for a particular package.
@@ -32,6 +36,38 @@ public final class PackageRollbackInfo implements Parcelable {
    private final VersionedPackage mVersionRolledBackFrom;
    private final VersionedPackage mVersionRolledBackTo;

    /**
     * Encapsulates information required to restore a snapshot of an app's userdata.
     *
     * @hide
     */
    public static class RestoreInfo {
        public final int userId;
        public final int appId;
        public final String seInfo;

        public RestoreInfo(int userId, int appId, String seInfo) {
            this.userId = userId;
            this.appId = appId;
            this.seInfo = seInfo;
        }
    }

    /*
     * The list of users for which we need to backup userdata for this package. Backups of
     * credential encrypted data are listed as pending if the user hasn't unlocked their device
     * with credentials yet.
     */
    // NOTE: Not a part of the Parcelable representation of this object.
    private final IntArray mPendingBackups;

    /**
     * The list of users for which we need to restore userdata for this package. This field is
     * non-null only after a rollback for this package has been committed.
     */
    // NOTE: Not a part of the Parcelable representation of this object.
    private final ArrayList<RestoreInfo> mPendingRestores;

    /**
     * Returns the name of the package to roll back from.
     */
@@ -53,16 +89,47 @@ public final class PackageRollbackInfo implements Parcelable {
        return mVersionRolledBackTo;
    }

    /** @hide */
    public IntArray getPendingBackups() {
        return mPendingBackups;
    }

    /** @hide */
    public ArrayList<RestoreInfo> getPendingRestores() {
        return mPendingRestores;
    }

    /** @hide */
    public RestoreInfo getRestoreInfo(int userId) {
        for (RestoreInfo ri : mPendingRestores) {
            if (ri.userId == userId) {
                return ri;
            }
        }

        return null;
    }

    /** @hide */
    public void removeRestoreInfo(RestoreInfo ri) {
        mPendingRestores.remove(ri);
    }

    /** @hide */
    public PackageRollbackInfo(VersionedPackage packageRolledBackFrom,
            VersionedPackage packageRolledBackTo) {
            VersionedPackage packageRolledBackTo,
            @NonNull IntArray pendingBackups, @NonNull ArrayList<RestoreInfo> pendingRestores) {
        this.mVersionRolledBackFrom = packageRolledBackFrom;
        this.mVersionRolledBackTo = packageRolledBackTo;
        this.mPendingBackups = pendingBackups;
        this.mPendingRestores = pendingRestores;
    }

    private PackageRollbackInfo(Parcel in) {
        this.mVersionRolledBackFrom = VersionedPackage.CREATOR.createFromParcel(in);
        this.mVersionRolledBackTo = VersionedPackage.CREATOR.createFromParcel(in);
        this.mPendingRestores = null;
        this.mPendingBackups = null;
    }

    @Override
+236 −0
Original line number Diff line number Diff line
/*
 * 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.
 */

package com.android.server.rollback;

import android.content.rollback.PackageRollbackInfo;
import android.content.rollback.PackageRollbackInfo.RestoreInfo;
import android.content.rollback.RollbackInfo;
import android.os.storage.StorageManager;
import android.util.IntArray;
import android.util.Log;

import com.android.internal.annotations.VisibleForTesting;
import com.android.server.pm.Installer;
import com.android.server.pm.Installer.InstallerException;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * Encapsulates the logic for initiating userdata snapshots and rollbacks via installd.
 */
@VisibleForTesting
// TODO(narayan): Reason about the failure scenarios that involve one or more IPCs to installd
// failing. We need to decide what course of action to take if calls to snapshotAppData or
// restoreAppDataSnapshot fail.
public class AppDataRollbackHelper {
    private static final String TAG = "RollbackManager";

    private final Installer mInstaller;

    public AppDataRollbackHelper(Installer installer) {
        mInstaller = installer;
    }

    /**
     * Creates an app data snapshot for a specified {@code packageName} for {@code installedUsers},
     * a specified set of users for whom the package is installed.
     *
     * @return a list of users for which the snapshot is pending, usually because data for one or
     *         more users is still credential locked.
     */
    public IntArray snapshotAppData(String packageName, int[] installedUsers) {
        final IntArray pendingBackups = new IntArray();
        for (int user : installedUsers) {
            final int storageFlags;
            if (isUserCredentialLocked(user)) {
                // We've encountered a user that hasn't unlocked on a FBE device, so we can't copy
                // across app user data until the user unlocks their device.
                Log.v(TAG, "User: " + user + " isn't unlocked, skipping CE userdata backup.");
                storageFlags = Installer.FLAG_STORAGE_DE;
                pendingBackups.add(user);
            } else {
                storageFlags = Installer.FLAG_STORAGE_CE | Installer.FLAG_STORAGE_DE;
            }

            try {
                mInstaller.snapshotAppData(packageName, user, storageFlags);
            } catch (InstallerException ie) {
                Log.e(TAG, "Unable to create app data snapshot for: " + packageName
                        + ", userId: " + user, ie);
            }
        }

        return pendingBackups;
    }

    /**
     * Restores an app data snapshot for a specified package ({@code packageName},
     * {@code rollbackData}) for a specified {@code userId}.
     *
     * @return {@code true} iff. a change to the {@code rollbackData} has been made. Changes to
     *         {@code rollbackData} are restricted to the removal or addition of {@code userId} to
     *         the list of pending backups or restores.
     */
    public boolean restoreAppData(String packageName, RollbackData rollbackData,
            int userId, int appId, long ceDataInode, String seInfo) {
        if (rollbackData == null) {
            return false;
        }

        if (!rollbackData.inProgress) {
            Log.e(TAG, "Request to restore userData for: " + packageName
                    + ", but no rollback in progress.");
            return false;
        }

        PackageRollbackInfo packageInfo = RollbackManagerServiceImpl.getPackageRollbackInfo(
                rollbackData, packageName);
        int storageFlags = Installer.FLAG_STORAGE_DE;

        final IntArray pendingBackups = packageInfo.getPendingBackups();
        final List<RestoreInfo> pendingRestores = packageInfo.getPendingRestores();
        boolean changedRollbackData = false;

        // If we still have a userdata backup pending for this user, it implies that the user
        // hasn't unlocked their device between the point of backup and the point of restore,
        // so the data cannot have changed. We simply skip restoring CE data in this case.
        if (pendingBackups != null && pendingBackups.indexOf(userId) != -1) {
            pendingBackups.remove(pendingBackups.indexOf(userId));
            changedRollbackData = true;
        } else {
            // There's no pending CE backup for this user, which means that we successfully
            // managed to backup data for the user, which means we seek to restore it
            if (isUserCredentialLocked(userId)) {
                // We've encountered a user that hasn't unlocked on a FBE device, so we can't
                // copy across app user data until the user unlocks their device.
                pendingRestores.add(new RestoreInfo(userId, appId, seInfo));
                changedRollbackData = true;
            } else {
                // This user has unlocked, we can proceed to restore both CE and DE data.
                storageFlags = storageFlags | Installer.FLAG_STORAGE_CE;
            }
        }

        try {
            mInstaller.restoreAppDataSnapshot(packageName, appId, ceDataInode,
                    seInfo, userId, storageFlags);
        } catch (InstallerException ie) {
            Log.e(TAG, "Unable to restore app data snapshot: " + packageName, ie);
        }

        return changedRollbackData;
    }

    /**
     * Computes the list of pending backups and restores for {@code userId} given lists of
     * available and recent rollbacks. Packages pending backup for the given user are added
     * to {@code pendingBackups} and packages pending restore are added to {@code pendingRestores}
     * along with their corresponding {@code RestoreInfo}.
     *
     * @return the list of {@code RollbackData} that have been modified during this computation.
     */
    public List<RollbackData> computePendingBackupsAndRestores(int userId,
            ArrayList<String> pendingBackupPackages, Map<String, RestoreInfo> pendingRestores,
            List<RollbackData> availableRollbacks, List<RollbackInfo> recentRollbacks) {
        List<RollbackData> rd = new ArrayList<>();
        // First check with the list of available rollbacks to see whether there are any
        // pending backup operations that we've not managed to execute.
        for (RollbackData data : availableRollbacks) {
            for (PackageRollbackInfo info : data.packages) {
                final IntArray pendingBackupUsers = info.getPendingBackups();
                if (pendingBackupUsers != null) {
                    final int idx = pendingBackupUsers.indexOf(userId);
                    if (idx != -1) {
                        pendingBackupPackages.add(info.getPackageName());
                        pendingBackupUsers.remove(idx);
                        if (rd.indexOf(data) == -1) {
                            rd.add(data);
                        }
                    }
                }
            }
        }

        // Then check with the list of recently executed rollbacks to see whether there are
        // any rollback operations
        for (RollbackInfo data : recentRollbacks) {
            for (PackageRollbackInfo info : data.getPackages()) {
                final RestoreInfo ri = info.getRestoreInfo(userId);
                if (ri != null) {
                    if (pendingBackupPackages.contains(info.getPackageName())) {
                        // This implies that the user hasn't unlocked their device between
                        // the request to backup data for this user and the request to restore
                        // it, so we do nothing here.
                        pendingBackupPackages.remove(info.getPackageName());
                    } else {
                        pendingRestores.put(info.getPackageName(), ri);
                    }

                    info.removeRestoreInfo(ri);
                }
            }
        }

        return rd;
    }

    /**
     * Commits the list of pending backups and restores for a given {@code userId}.
     */
    public void commitPendingBackupAndRestoreForUser(int userId,
            ArrayList<String> pendingBackups, Map<String, RestoreInfo> pendingRestores) {
        if (!pendingBackups.isEmpty()) {
            for (String packageName : pendingBackups) {
                try {
                    mInstaller.snapshotAppData(packageName, userId, Installer.FLAG_STORAGE_CE);
                } catch (InstallerException ie) {
                    Log.e(TAG, "Unable to create app data snapshot for: " + packageName, ie);
                }
            }
        }

        // TODO(narayan): Should we perform the restore before the backup for packages that have
        // both backups and restores pending ? We could get into this case if we have a pending
        // restore from a rollback + a snapshot request from a new restore.
        if (!pendingRestores.isEmpty()) {
            for (String packageName : pendingRestores.keySet()) {
                try {
                    final RestoreInfo ri = pendingRestores.get(packageName);

                    // TODO(narayan): Verify that the user of "0" for ceDataInode is accurate
                    // here. We know that the user has unlocked (and that their CE data is
                    // available) so we shouldn't need to resort to the fallback path.
                    mInstaller.restoreAppDataSnapshot(packageName, ri.appId,
                            0 /* ceDataInode */, ri.seInfo, userId, Installer.FLAG_STORAGE_CE);
                } catch (InstallerException ie) {
                    Log.e(TAG, "Unable to restore app data snapshot for: " + packageName, ie);
                }
            }
        }
    }

    /**
     * @return {@code true} iff. {@code userId} is locked on an FBE device.
     */
    @VisibleForTesting
    public boolean isUserCredentialLocked(int userId) {
        return StorageManager.isFileEncryptedNativeOrEmulated()
                && !StorageManager.isUserKeyUnlocked(userId);
    }
}
+5 −0
Original line number Diff line number Diff line
@@ -39,4 +39,9 @@ public final class RollbackManagerService extends SystemService {
        mService = new RollbackManagerServiceImpl(getContext());
        publishBinderService(Context.ROLLBACK_SERVICE, mService);
    }

    @Override
    public void onUnlockUser(int user) {
        mService.onUnlockUser(user);
    }
}
+84 −56
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@ import android.content.pm.ParceledListSlice;
import android.content.pm.VersionedPackage;
import android.content.rollback.IRollbackManager;
import android.content.rollback.PackageRollbackInfo;
import android.content.rollback.PackageRollbackInfo.RestoreInfo;
import android.content.rollback.RollbackInfo;
import android.content.rollback.RollbackManager;
import android.os.Binder;
@@ -39,14 +40,13 @@ import android.os.Handler;
import android.os.HandlerThread;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.storage.StorageManager;
import android.util.IntArray;
import android.util.Log;
import android.util.SparseBooleanArray;

import com.android.internal.annotations.GuardedBy;
import com.android.server.LocalServices;
import com.android.server.pm.Installer;
import com.android.server.pm.Installer.InstallerException;
import com.android.server.pm.PackageManagerServiceUtils;

import java.io.File;
@@ -111,6 +111,7 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
    private final HandlerThread mHandlerThread;
    private final Installer mInstaller;
    private final RollbackPackageHealthObserver mPackageHealthObserver;
    private final AppDataRollbackHelper mUserdataHelper;

    RollbackManagerServiceImpl(Context context) {
        mContext = context;
@@ -124,6 +125,7 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
        mRollbackStore = new RollbackStore(new File(Environment.getDataDirectory(), "rollback"));

        mPackageHealthObserver = new RollbackPackageHealthObserver(mContext);
        mUserdataHelper = new AppDataRollbackHelper(mInstaller);

        // Kick off loading of the rollback data from strorage in a background
        // thread.
@@ -424,6 +426,35 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
        }
    }

    void onUnlockUser(int userId) {
        getHandler().post(() -> {
            final ArrayList<String> pendingBackupPackages = new ArrayList<>();
            final Map<String, RestoreInfo> pendingRestorePackages = new HashMap<>();
            final List<RollbackData> changed;
            synchronized (mLock) {
                ensureRollbackDataLoadedLocked();
                changed = mUserdataHelper.computePendingBackupsAndRestores(userId,
                        pendingBackupPackages, pendingRestorePackages, mAvailableRollbacks,
                        mRecentlyExecutedRollbacks);
            }

            mUserdataHelper.commitPendingBackupAndRestoreForUser(userId,
                    pendingBackupPackages, pendingRestorePackages);

            for (RollbackData rd : changed) {
                try {
                    mRollbackStore.saveAvailableRollback(rd);
                } catch (IOException ioe) {
                    Log.e(TAG, "Unable to save rollback info for : " + rd.rollbackId, ioe);
                }
            }

            synchronized (mLock) {
                mRollbackStore.saveRecentlyExecutedRollbacks(mRecentlyExecutedRollbacks);
            }
        });
    }

    /**
     * Load rollback data from storage if it has not already been loaded.
     * After calling this funciton, mAvailableRollbacks and
@@ -533,6 +564,20 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
        // that are necessary to keep track of.
        synchronized (mLock) {
            ensureRollbackDataLoadedLocked();

            // This should never happen because we can't have any pending backups left after
            // a rollback has been executed. See AppDataRollbackHelper#restoreAppData where we
            // clear all pending backups at the point of restore because they're guaranteed to be
            // no-ops.
            //
            // We may, however, have one or more pending restores left to handle.
            for (PackageRollbackInfo target : rollback.getPackages()) {
                if (target.getPendingBackups().size() > 0) {
                    Log.e(TAG, "No backups allowed to be pending for: " + target);
                    target.getPendingBackups().clear();
                }
            }

            mRecentlyExecutedRollbacks.add(rollback);
            mRollbackStore.saveRecentlyExecutedRollbacks(mRecentlyExecutedRollbacks);
        }
@@ -701,27 +746,12 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
        VersionedPackage installedVersion = new VersionedPackage(packageName,
                installedPackage.getLongVersionCode());

        for (int user : installedUsers) {
            final int storageFlags;
            if (StorageManager.isFileEncryptedNativeOrEmulated()
                    && !StorageManager.isUserKeyUnlocked(user)) {
                // We've encountered a user that hasn't unlocked on a FBE device, so we can't copy
                // across app user data until the user unlocks their device.
                Log.e(TAG, "User: " + user + " isn't unlocked, skipping CE userdata backup.");
                storageFlags = Installer.FLAG_STORAGE_DE;
            } else {
                storageFlags = Installer.FLAG_STORAGE_CE | Installer.FLAG_STORAGE_DE;
            }

            try {
                mInstaller.snapshotAppData(packageName, user, storageFlags);
            } catch (InstallerException ie) {
                Log.e(TAG, "Unable to create app data snapshot for: " + packageName, ie);
            }
        }

        PackageRollbackInfo info = new PackageRollbackInfo(newVersion, installedVersion);
        final IntArray pendingBackups = mUserdataHelper.snapshotAppData(packageName,
                installedUsers);

        PackageRollbackInfo info = new PackageRollbackInfo(newVersion, installedVersion,
                pendingBackups, new ArrayList<>());
        RollbackData data;
        try {
            synchronized (mLock) {
@@ -760,40 +790,24 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
        }

        getHandler().post(() -> {
            PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class);
            final RollbackData rollbackData = getRollbackForPackage(packageName);
            if (rollbackData == null) {
                pmi.finishPackageInstall(token, false);
                return;
            }

            if (!rollbackData.inProgress) {
                Log.e(TAG, "Request to restore userData for: " + packageName
                        + ", but no rollback in progress.");
            final boolean changedRollbackData = mUserdataHelper.restoreAppData(packageName,
                    rollbackData, userId, appId, ceDataInode, seInfo);
            final PackageManagerInternal pmi = LocalServices.getService(
                    PackageManagerInternal.class);
            pmi.finishPackageInstall(token, false);
                return;
            }

            final int storageFlags;
            if (StorageManager.isFileEncryptedNativeOrEmulated()
                    && !StorageManager.isUserKeyUnlocked(userId)) {
                // We've encountered a user that hasn't unlocked on a FBE device, so we can't copy
                // across app user data until the user unlocks their device.
                Log.e(TAG, "User: " + userId + " isn't unlocked, skipping CE userdata restore.");

                storageFlags = Installer.FLAG_STORAGE_DE;
            } else {
                storageFlags = Installer.FLAG_STORAGE_CE | Installer.FLAG_STORAGE_DE;
            }

            // We've updated metadata about this rollback, so save it to flash.
            if (changedRollbackData) {
                try {
                mInstaller.restoreAppDataSnapshot(packageName, appId, ceDataInode,
                        seInfo, userId, storageFlags);
            } catch (InstallerException ie) {
                Log.e(TAG, "Unable to restore app data snapshot: " + packageName, ie);
                    mRollbackStore.saveAvailableRollback(rollbackData);
                } catch (IOException ioe) {
                    // TODO(narayan): What is the right thing to do here ? This isn't a fatal error,
                    // since it will only result in us trying to restore data again, which will be
                    // a no-op if there's no data available.
                    Log.e(TAG, "Unable to save available rollback: " + packageName, ioe);
                }
            }

            pmi.finishPackageInstall(token, false);
        });
    }

@@ -900,13 +914,11 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
            ensureRollbackDataLoadedLocked();
            for (int i = 0; i < mAvailableRollbacks.size(); ++i) {
                RollbackData data = mAvailableRollbacks.get(i);
                for (PackageRollbackInfo info : data.packages) {
                    if (info.getPackageName().equals(packageName)) {
                if (getPackageRollbackInfo(data, packageName) != null) {
                    return data;
                }
            }
        }
        }
        return null;
    }

@@ -926,6 +938,22 @@ class RollbackManagerServiceImpl extends IRollbackManager.Stub {
                }
            }
        }

        return null;
    }

    /**
     * Returns the {@code PackageRollbackInfo} associated with {@code packageName} from
     * a specified {@code RollbackData}.
     */
    static PackageRollbackInfo getPackageRollbackInfo(RollbackData data,
            String packageName) {
        for (PackageRollbackInfo info : data.packages) {
            if (info.getPackageName().equals(packageName)) {
                return info;
            }
        }

        return null;
    }

+75 −1

File changed.

Preview size limit exceeded, changes collapsed.

Loading