diff options
5 files changed, 294 insertions, 32 deletions
diff --git a/core/java/android/os/IUserManager.aidl b/core/java/android/os/IUserManager.aidl index 07363edd3e75..6fe57774f6f3 100644 --- a/core/java/android/os/IUserManager.aidl +++ b/core/java/android/os/IUserManager.aidl @@ -86,6 +86,7 @@ interface IUserManager { Bundle getApplicationRestrictionsForUser(in String packageName, int userId); void setDefaultGuestRestrictions(in Bundle restrictions); Bundle getDefaultGuestRestrictions(); + int removeUserOrSetEphemeral(int userId); boolean markGuestForDeletion(int userId); UserInfo findCurrentGuestUser(); boolean isQuietModeEnabled(int userId); diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index ddc21ab2c8c0..42ae93030f38 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -3978,6 +3978,23 @@ public class UserManager { } /** + * Immediately removes the user or, if the user cannot be removed, such as when the user is + * the current user, then set the user as ephemeral so that it will be removed when it is + * stopped. + * + * @return the {@link com.android.server.pm.UserManagerService.RemoveResult} code + * @hide + */ + @RequiresPermission(android.Manifest.permission.MANAGE_USERS) + public int removeUserOrSetEphemeral(@UserIdInt int userId) { + try { + return mService.removeUserOrSetEphemeral(userId); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + + /** * Updates the user's name. * * @param userId the user's integer id diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index c356fc72379f..c46a7efaa704 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -24,6 +24,7 @@ import static android.content.pm.PackageManager.INTENT_FILTER_DOMAIN_VERIFICATIO import static android.content.pm.PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED; import android.accounts.IAccountManager; +import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.app.role.IRoleManager; @@ -2686,9 +2687,18 @@ class PackageManagerShellCommand extends ShellCommand { } } + // pm remove-user [--set-ephemeral-if-in-use] USER_ID public int runRemoveUser() throws RemoteException { int userId; - String arg = getNextArg(); + String arg; + boolean setEphemeralIfInUse = false; + while ((arg = getNextOption()) != null) { + if (arg.equals("--set-ephemeral-if-in-use")) { + setEphemeralIfInUse = true; + } + } + + arg = getNextArg(); if (arg == null) { getErrPrintWriter().println("Error: no user id specified."); return 1; @@ -2696,6 +2706,15 @@ class PackageManagerShellCommand extends ShellCommand { userId = UserHandle.parseUserArg(arg); IUserManager um = IUserManager.Stub.asInterface( ServiceManager.getService(Context.USER_SERVICE)); + if (setEphemeralIfInUse) { + return removeUserOrSetEphemeral(um, userId); + } else { + return removeUser(um, userId); + } + } + + private int removeUser(IUserManager um, @UserIdInt int userId) throws RemoteException { + Slog.i(TAG, "Removing user " + userId); if (um.removeUser(userId)) { getOutPrintWriter().println("Success: removed user"); return 0; @@ -2705,6 +2724,27 @@ class PackageManagerShellCommand extends ShellCommand { } } + private int removeUserOrSetEphemeral(IUserManager um, @UserIdInt int userId) + throws RemoteException { + Slog.i(TAG, "Removing " + userId + " or set as ephemeral if in use."); + int result = um.removeUserOrSetEphemeral(userId); + switch (result) { + case UserManagerService.REMOVE_RESULT_REMOVED: + getOutPrintWriter().printf("Success: user %d removed\n", userId); + return 0; + case UserManagerService.REMOVE_RESULT_SET_EPHEMERAL: + getOutPrintWriter().printf("Success: user %d set as ephemeral\n", userId); + return 0; + case UserManagerService.REMOVE_RESULT_ALREADY_BEING_REMOVED: + getOutPrintWriter().printf("Success: user %d is already being removed\n", userId); + return 0; + default: + getErrPrintWriter().printf("Error: couldn't remove or mark ephemeral user id %d\n", + userId); + return 1; + } + } + public int runSetUserRestriction() throws RemoteException { int userId = UserHandle.USER_SYSTEM; String opt = getNextOption(); @@ -3769,9 +3809,13 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" --restricted is shorthand for '--user-type android.os.usertype.full.RESTRICTED'."); pw.println(" --guest is shorthand for '--user-type android.os.usertype.full.GUEST'."); pw.println(""); - pw.println(" remove-user USER_ID"); + pw.println(" remove-user [--set-ephemeral-if-in-use] USER_ID"); pw.println(" Remove the user with the given USER_IDENTIFIER, deleting all data"); - pw.println(" associated with that user"); + pw.println(" associated with that user."); + pw.println(" --set-ephemeral-if-in-use: If the user is currently running and"); + pw.println(" therefore cannot be removed immediately, mark the user as ephemeral"); + pw.println(" so that it will be automatically removed when possible (after user"); + pw.println(" switch or reboot)"); pw.println(""); pw.println(" set-user-restriction [--user USER_ID] RESTRICTION VALUE"); pw.println(""); diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 3ec156f6a3a1..766b30f1dc21 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -22,6 +22,7 @@ import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import android.Manifest; import android.annotation.ColorRes; import android.annotation.DrawableRes; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.StringRes; @@ -110,7 +111,6 @@ import com.android.internal.widget.LockPatternUtils; import com.android.server.LocalServices; import com.android.server.LockGuard; import com.android.server.SystemService; -import com.android.server.SystemService.TargetUser; import com.android.server.am.UserState; import com.android.server.storage.DeviceStorageMonitorInternal; import com.android.server.utils.TimingsTraceAndSlog; @@ -132,6 +132,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; @@ -246,6 +248,43 @@ public class UserManagerService extends IUserManager.Stub { static final int WRITE_USER_MSG = 1; static final int WRITE_USER_DELAY = 2*1000; // 2 seconds + /** + * A response code from {@link #removeUserOrSetEphemeral(int)} indicating that the specified + * user has been successfully removed. + */ + public static final int REMOVE_RESULT_REMOVED = 0; + + /** + * A response code from {@link #removeUserOrSetEphemeral(int)} indicating that the specified + * user has had its {@link UserInfo#FLAG_EPHEMERAL} flag set to {@code true}, so that it will be + * removed when the user is stopped or on boot. + */ + public static final int REMOVE_RESULT_SET_EPHEMERAL = 1; + + /** + * A response code from {@link #removeUserOrSetEphemeral(int)} indicating that the specified + * user is already in the process of being removed. + */ + public static final int REMOVE_RESULT_ALREADY_BEING_REMOVED = 2; + + /** + * A response code from {@link #removeUserOrSetEphemeral(int)} indicating that an error occurred + * that prevented the user from being removed or set as ephemeral. + */ + public static final int REMOVE_RESULT_ERROR = 3; + + /** + * Possible response codes from {@link #removeUserOrSetEphemeral(int)}. + */ + @IntDef(prefix = { "REMOVE_RESULT_" }, value = { + REMOVE_RESULT_REMOVED, + REMOVE_RESULT_SET_EPHEMERAL, + REMOVE_RESULT_ALREADY_BEING_REMOVED, + REMOVE_RESULT_ERROR, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface RemoveResult {} + // Tron counters private static final String TRON_GUEST_CREATED = "users_guest_created"; private static final String TRON_USER_CREATED = "users_user_created"; @@ -253,9 +292,17 @@ public class UserManagerService extends IUserManager.Stub { private final Context mContext; private final PackageManagerService mPm; + + /** + * Lock for packages. If using with {@link #mUsersLock}, {@link #mPackagesLock} should be + * acquired first. + */ private final Object mPackagesLock; private final UserDataPreparer mUserDataPreparer; - // Short-term lock for internal state, when interaction/sync with PM is not required + /** + * Short-term lock for internal state, when interaction/sync with PM is not required. If using + * with {@link #mPackagesLock}, {@link #mPackagesLock} should be acquired first. + */ private final Object mUsersLock = LockGuard.installNewLock(LockGuard.INDEX_USER); private final Object mRestrictionsLock = new Object(); // Used for serializing access to app restriction files @@ -3862,13 +3909,7 @@ public class UserManagerService extends IUserManager.Stub { Slog.i(LOG_TAG, "removeUser u" + userId); checkManageOrCreateUsersPermission("Only the system can remove users"); - final boolean isManagedProfile; - synchronized (mUsersLock) { - UserInfo userInfo = getUserInfoLU(userId); - isManagedProfile = userInfo != null && userInfo.isManagedProfile(); - } - String restriction = isManagedProfile - ? UserManager.DISALLOW_REMOVE_MANAGED_PROFILE : UserManager.DISALLOW_REMOVE_USER; + final String restriction = getUserRemovalRestriction(userId); if (getUserRestrictions(UserHandle.getCallingUserId()).getBoolean(restriction, false)) { Slog.w(LOG_TAG, "Cannot remove user. " + restriction + " is enabled."); return false; @@ -3882,6 +3923,21 @@ public class UserManagerService extends IUserManager.Stub { return removeUserUnchecked(userId); } + /** + * Returns the string name of the restriction to check for user removal. The restriction name + * varies depending on whether the user is a managed profile. + */ + private String getUserRemovalRestriction(@UserIdInt int userId) { + final boolean isManagedProfile; + final UserInfo userInfo; + synchronized (mUsersLock) { + userInfo = getUserInfoLU(userId); + } + isManagedProfile = userInfo != null && userInfo.isManagedProfile(); + return isManagedProfile + ? UserManager.DISALLOW_REMOVE_MANAGED_PROFILE : UserManager.DISALLOW_REMOVE_USER; + } + private boolean removeUserUnchecked(@UserIdInt int userId) { final long ident = Binder.clearCallingIdentity(); try { @@ -3974,6 +4030,64 @@ public class UserManagerService extends IUserManager.Stub { } } + @Override + public @RemoveResult int removeUserOrSetEphemeral(@UserIdInt int userId) { + Slog.i(LOG_TAG, "removeUserOrSetEphemeral u" + userId); + checkManageUsersPermission("Only the system can remove users"); + final String restriction = getUserRemovalRestriction(userId); + if (getUserRestrictions(UserHandle.getCallingUserId()).getBoolean(restriction, false)) { + Slog.w(LOG_TAG, "Cannot remove user. " + restriction + " is enabled."); + return REMOVE_RESULT_ERROR; + } + if (userId == UserHandle.USER_SYSTEM) { + Slog.e(LOG_TAG, "System user cannot be removed."); + return REMOVE_RESULT_ERROR; + } + + final long ident = Binder.clearCallingIdentity(); + try { + final UserData userData; + synchronized (mPackagesLock) { + synchronized (mUsersLock) { + userData = mUsers.get(userId); + if (userData == null) { + Slog.e(LOG_TAG, + "Cannot remove user " + userId + ", invalid user id provided."); + return REMOVE_RESULT_ERROR; + } + + if (mRemovingUserIds.get(userId)) { + Slog.e(LOG_TAG, "User " + userId + " is already scheduled for removal."); + return REMOVE_RESULT_ALREADY_BEING_REMOVED; + } + } + + // Attempt to immediately remove a non-current user + final int currentUser = ActivityManager.getCurrentUser(); + if (currentUser != userId) { + // Attempt to remove the user. This will fail if the user is the current user + if (removeUser(userId)) { + return REMOVE_RESULT_REMOVED; + } + + Slog.w(LOG_TAG, "Unable to immediately remove non-current user: " + userId + + ". User is still set as ephemeral and will be removed on user " + + "switch or reboot."); + } + + // If the user was not immediately removed, make sure it is marked as ephemeral. + // Don't mark as disabled since, per UserInfo.FLAG_DISABLED documentation, an + // ephemeral user should only be marked as disabled when its removal is in progress. + userData.info.flags |= UserInfo.FLAG_EPHEMERAL; + writeUserLP(userData); + + return REMOVE_RESULT_SET_EPHEMERAL; + } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + void finishRemoveUser(final @UserIdInt int userId) { if (DBG) Slog.i(LOG_TAG, "finishRemoveUser " + userId); diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java index 22b07157e94e..75799562abfa 100644 --- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java @@ -41,6 +41,7 @@ import android.test.suitebuilder.annotation.MediumTest; import android.test.suitebuilder.annotation.SmallTest; import android.util.Slog; +import androidx.annotation.Nullable; import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; @@ -58,6 +59,8 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import javax.annotation.concurrent.GuardedBy; + /** Test {@link UserManager} functionality. */ @RunWith(AndroidJUnit4.class) public final class UserManagerTest { @@ -134,7 +137,7 @@ public final class UserManagerTest { @SmallTest @Test public void testHasSystemUser() throws Exception { - assertThat(findUser(UserHandle.USER_SYSTEM)).isTrue(); + assertThat(hasUser(UserHandle.USER_SYSTEM)).isTrue(); } @MediumTest @@ -164,9 +167,9 @@ public final class UserManagerTest { assertThat(user1).isNotNull(); assertThat(user2).isNotNull(); - assertThat(findUser(UserHandle.USER_SYSTEM)).isTrue(); - assertThat(findUser(user1.id)).isTrue(); - assertThat(findUser(user2.id)).isTrue(); + assertThat(hasUser(UserHandle.USER_SYSTEM)).isTrue(); + assertThat(hasUser(user1.id)).isTrue(); + assertThat(hasUser(user2.id)).isTrue(); } @MediumTest @@ -175,7 +178,7 @@ public final class UserManagerTest { UserInfo userInfo = createUser("Guest 1", UserInfo.FLAG_GUEST); removeUser(userInfo.id); - assertThat(findUser(userInfo.id)).isFalse(); + assertThat(hasUser(userInfo.id)).isFalse(); } @MediumTest @@ -199,7 +202,7 @@ public final class UserManagerTest { } } - assertThat(findUser(userInfo.id)).isFalse(); + assertThat(hasUser(userInfo.id)).isFalse(); } @MediumTest @@ -208,6 +211,79 @@ public final class UserManagerTest { assertThrows(IllegalArgumentException.class, () -> mUserManager.removeUser(null)); } + @MediumTest + @Test + public void testRemoveUserOrSetEphemeral_restrictedReturnsError() throws Exception { + final int currentUser = ActivityManager.getCurrentUser(); + final UserInfo user1 = createUser("User 1", /* flags= */ 0); + mUserManager.setUserRestriction(UserManager.DISALLOW_REMOVE_USER, /* value= */ true, + asHandle(currentUser)); + try { + assertThat(mUserManager.removeUserOrSetEphemeral(user1.id)).isEqualTo( + UserManagerService.REMOVE_RESULT_ERROR); + } finally { + mUserManager.setUserRestriction(UserManager.DISALLOW_REMOVE_USER, /* value= */ false, + asHandle(currentUser)); + } + + assertThat(hasUser(user1.id)).isTrue(); + assertThat(getUser(user1.id).isEphemeral()).isFalse(); + } + + @MediumTest + @Test + public void testRemoveUserOrSetEphemeral_systemUserReturnsError() throws Exception { + assertThat(mUserManager.removeUserOrSetEphemeral(UserHandle.USER_SYSTEM)).isEqualTo( + UserManagerService.REMOVE_RESULT_ERROR); + + assertThat(hasUser(UserHandle.USER_SYSTEM)).isTrue(); + } + + @MediumTest + @Test + public void testRemoveUserOrSetEphemeral_invalidUserReturnsError() throws Exception { + assertThat(hasUser(Integer.MAX_VALUE)).isFalse(); + assertThat(mUserManager.removeUserOrSetEphemeral(Integer.MAX_VALUE)).isEqualTo( + UserManagerService.REMOVE_RESULT_ERROR); + } + + @MediumTest + @Test + public void testRemoveUserOrSetEphemeral_currentUserSetEphemeral() throws Exception { + final int startUser = ActivityManager.getCurrentUser(); + final UserInfo user1 = createUser("User 1", /* flags= */ 0); + // Switch to the user just created. + switchUser(user1.id, null, /* ignoreHandle= */ true); + + assertThat(mUserManager.removeUserOrSetEphemeral(user1.id)).isEqualTo( + UserManagerService.REMOVE_RESULT_SET_EPHEMERAL); + + assertThat(hasUser(user1.id)).isTrue(); + assertThat(getUser(user1.id).isEphemeral()).isTrue(); + + // Switch back to the starting user. + switchUser(startUser, null, /* ignoreHandle= */ true); + + // User is removed once switch is complete + synchronized (mUserRemoveLock) { + waitForUserRemovalLocked(user1.id); + } + assertThat(hasUser(user1.id)).isFalse(); + } + + @MediumTest + @Test + public void testRemoveUserOrSetEphemeral_nonCurrentUserRemoved() throws Exception { + final UserInfo user1 = createUser("User 1", /* flags= */ 0); + synchronized (mUserRemoveLock) { + assertThat(mUserManager.removeUserOrSetEphemeral(user1.id)).isEqualTo( + UserManagerService.REMOVE_RESULT_REMOVED); + waitForUserRemovalLocked(user1.id); + } + + assertThat(hasUser(user1.id)).isFalse(); + } + /** Tests creating a FULL user via specifying userType. */ @MediumTest @Test @@ -608,15 +684,20 @@ public final class UserManagerTest { () -> mUserManager.getUserCreationTime(asHandle(user.id))); } - private boolean findUser(int id) { + @Nullable + private UserInfo getUser(int id) { List<UserInfo> list = mUserManager.getUsers(); for (UserInfo user : list) { if (user.id == id) { - return true; + return user; } } - return false; + return null; + } + + private boolean hasUser(int id) { + return getUser(id) != null; } @MediumTest @@ -918,17 +999,22 @@ public final class UserManagerTest { private void removeUser(int userId) { synchronized (mUserRemoveLock) { mUserManager.removeUser(userId); - long time = System.currentTimeMillis(); - while (mUserManager.getUserInfo(userId) != null) { - try { - mUserRemoveLock.wait(REMOVE_CHECK_INTERVAL_MILLIS); - } catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - return; - } - if (System.currentTimeMillis() - time > REMOVE_TIMEOUT_MILLIS) { - fail("Timeout waiting for removeUser. userId = " + userId); - } + waitForUserRemovalLocked(userId); + } + } + + @GuardedBy("mUserRemoveLock") + private void waitForUserRemovalLocked(int userId) { + long time = System.currentTimeMillis(); + while (mUserManager.getUserInfo(userId) != null) { + try { + mUserRemoveLock.wait(REMOVE_CHECK_INTERVAL_MILLIS); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + return; + } + if (System.currentTimeMillis() - time > REMOVE_TIMEOUT_MILLIS) { + fail("Timeout waiting for removeUser. userId = " + userId); } } } |