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

Commit bfce228e authored by Fyodor Kupolov's avatar Fyodor Kupolov Committed by Android (Google) Code Review
Browse files

Merge "Recycle userIds after hitting the limit"

parents bf77013b 0f4533d8
Loading
Loading
Loading
Loading
+91 −23
Original line number Diff line number Diff line
@@ -113,6 +113,7 @@ import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

/**
@@ -130,6 +131,8 @@ public class UserManagerService extends IUserManager.Stub {
    private static final String LOG_TAG = "UserManagerService";
    static final boolean DBG = false; // DO NOT SUBMIT WITH TRUE
    private static final boolean DBG_WITH_STACKTRACE = false; // DO NOT SUBMIT WITH TRUE
    // Can be used for manual testing of id recycling
    private static final boolean RELEASE_DELETED_USER_ID = false; // DO NOT SUBMIT WITH TRUE

    private static final String TAG_NAME = "name";
    private static final String TAG_ACCOUNT = "account";
@@ -183,9 +186,15 @@ public class UserManagerService extends IUserManager.Stub {
            | UserInfo.FLAG_GUEST
            | UserInfo.FLAG_DEMO;

    private static final int MIN_USER_ID = 10;
    @VisibleForTesting
    static final int MIN_USER_ID = 10;
    // We need to keep process uid within Integer.MAX_VALUE.
    private static final int MAX_USER_ID = Integer.MAX_VALUE / UserHandle.PER_USER_RANGE;
    @VisibleForTesting
    static final int MAX_USER_ID = Integer.MAX_VALUE / UserHandle.PER_USER_RANGE;

    // Max size of the queue of recently removed users
    @VisibleForTesting
    static final int MAX_RECENTLY_REMOVED_IDS_SIZE = 100;

    private static final int USER_VERSION = 6;

@@ -312,10 +321,17 @@ public class UserManagerService extends IUserManager.Stub {
    /**
     * Set of user IDs being actively removed. Removed IDs linger in this set
     * for several seconds to work around a VFS caching issue.
     * Use {@link #addRemovingUserIdLocked(int)} to add elements to this array
     */
    @GuardedBy("mUsersLock")
    private final SparseBooleanArray mRemovingUserIds = new SparseBooleanArray();

    /**
     * Queue of recently removed userIds. Used for recycling of userIds
     */
    @GuardedBy("mUsersLock")
    private final LinkedList<Integer> mRecentlyRemovedIds = new LinkedList<>();

    @GuardedBy("mUsersLock")
    private int[] mUserIds;
    @GuardedBy("mPackagesLock")
@@ -401,9 +417,10 @@ public class UserManagerService extends IUserManager.Stub {
        }
    }

    // TODO b/28848102 Add support for test dependencies injection
    @VisibleForTesting
    UserManagerService(File dataDir) {
        this(null, null, new Object(), dataDir);
    UserManagerService(Context context) {
        this(context, null, new Object(), context.getCacheDir());
    }

    /**
@@ -472,7 +489,7 @@ public class UserManagerService extends IUserManager.Stub {
                UserInfo ui = mUsers.valueAt(i).info;
                if ((ui.partial || ui.guestToRemove || ui.isEphemeral()) && i != 0) {
                    partials.add(ui);
                    mRemovingUserIds.append(ui.id, true);
                    addRemovingUserIdLocked(ui.id);
                    ui.partial = true;
                }
            }
@@ -1791,11 +1808,7 @@ public class UserManagerService extends IUserManager.Stub {
        }
        // Create the system user
        UserInfo system = new UserInfo(UserHandle.USER_SYSTEM, null, null, flags);
        UserData userData = new UserData();
        userData.info = system;
        synchronized (mUsersLock) {
            mUsers.put(system.id, userData);
        }
        UserData userData = putUserInfo(system);
        mNextSerialNumber = MIN_USER_ID;
        mUserVersion = USER_VERSION;

@@ -2335,6 +2348,23 @@ public class UserManagerService extends IUserManager.Stub {
        return userInfo;
    }

    @VisibleForTesting
    UserData putUserInfo(UserInfo userInfo) {
        final UserData userData = new UserData();
        userData.info = userInfo;
        synchronized (mUsers) {
            mUsers.put(userInfo.id, userData);
        }
        return userData;
    }

    @VisibleForTesting
    void removeUserInfo(int userId) {
        synchronized (mUsers) {
            mUsers.remove(userId);
        }
    }

    /**
     * @hide
     */
@@ -2451,10 +2481,7 @@ public class UserManagerService extends IUserManager.Stub {
                        return false;
                    }

                    // We remember deleted user IDs to prevent them from being
                    // reused during the current boot; they can still be reused
                    // after a reboot.
                    mRemovingUserIds.put(userHandle, true);
                    addRemovingUserIdLocked(userHandle);
                }

                try {
@@ -2501,6 +2528,19 @@ public class UserManagerService extends IUserManager.Stub {
        }
    }

    @VisibleForTesting
    void addRemovingUserIdLocked(int userId) {
        // We remember deleted user IDs to prevent them from being
        // reused during the current boot; they can still be reused
        // after a reboot or recycling of userIds.
        mRemovingUserIds.put(userId, true);
        mRecentlyRemovedIds.add(userId);
        // Keep LRU queue of recently removed IDs for recycling
        if (mRecentlyRemovedIds.size() > MAX_RECENTLY_REMOVED_IDS_SIZE) {
            mRecentlyRemovedIds.removeFirst();
        }
    }

    void finishRemoveUser(final int userHandle) {
        if (DBG) Slog.i(LOG_TAG, "finishRemoveUser " + userHandle);
        // Let other services shutdown any activity and clean up their state before completely
@@ -2586,6 +2626,11 @@ public class UserManagerService extends IUserManager.Stub {
        AtomicFile userFile = new AtomicFile(new File(mUsersDir, userHandle + XML_SUFFIX));
        userFile.delete();
        updateUserIds();
        if (RELEASE_DELETED_USER_ID) {
            synchronized (mUsers) {
                mRemovingUserIds.delete(userHandle);
            }
        }
    }

    private void sendProfileRemovedBroadcast(int parentUserId, int removedUserId) {
@@ -2966,21 +3011,40 @@ public class UserManagerService extends IUserManager.Stub {

    /**
     * Returns the next available user id, filling in any holes in the ids.
     * TODO: May not be a good idea to recycle ids, in case it results in confusion
     * for data and battery stats collection, or unexpected cross-talk.
     */
    private int getNextAvailableId() {
    @VisibleForTesting
    int getNextAvailableId() {
        int nextId;
        synchronized (mUsersLock) {
            int i = MIN_USER_ID;
            while (i < MAX_USER_ID) {
                if (mUsers.indexOfKey(i) < 0 && !mRemovingUserIds.get(i)) {
                    return i;
            nextId = scanNextAvailableIdLocked();
            if (nextId >= 0) {
                return nextId;
            }
                i++;
            // All ids up to MAX_USER_ID were used. Remove all mRemovingUserIds,
            // except most recently removed
            if (mRemovingUserIds.size() > 0) {
                Slog.i(LOG_TAG, "All available IDs are used. Recycling LRU ids.");
                mRemovingUserIds.clear();
                for (Integer recentlyRemovedId : mRecentlyRemovedIds) {
                    mRemovingUserIds.put(recentlyRemovedId, true);
                }
                nextId = scanNextAvailableIdLocked();
            }
        }
        if (nextId < 0) {
            throw new IllegalStateException("No user id available!");
        }
        return nextId;
    }

    private int scanNextAvailableIdLocked() {
        for (int i = MIN_USER_ID; i < MAX_USER_ID; i++) {
            if (mUsers.indexOfKey(i) < 0 && !mRemovingUserIds.get(i)) {
                return i;
            }
        }
        return -1;
    }

    private String packageToRestrictionsFileName(String packageName) {
        return RESTRICTIONS_FILE_PREFIX + packageName + XML_SUFFIX;
@@ -3284,6 +3348,10 @@ public class UserManagerService extends IUserManager.Stub {
            synchronized (mUsersLock) {
                pw.println();
                pw.println("  Device managed: " + mIsDeviceManaged);
                if (mRemovingUserIds.size() > 0) {
                    pw.println();
                    pw.println("  Recently removed userIds: " + mRecentlyRemovedIds);
                }
            }
            synchronized (mUserStates) {
                pw.println("  Started users state: " + mUserStates);
+124 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2016 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.pm;

import android.content.pm.UserInfo;
import android.os.Looper;
import android.os.UserManagerInternal;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import android.support.test.filters.MediumTest;

import com.android.server.LocalServices;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.LinkedHashSet;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

/**
 * <p>Run with:<pre>
 * m FrameworksServicesTests &&
 * adb install \
 * -r out/target/product/hammerhead/data/app/FrameworksServicesTests/FrameworksServicesTests.apk &&
 * adb shell am instrument -e class com.android.server.pm.UserManagerServiceIdRecyclingTest \
 * -w com.android.frameworks.servicestests/android.support.test.runner.AndroidJUnitRunner
 * </pre>
 */
@RunWith(AndroidJUnit4.class)
@MediumTest
public class UserManagerServiceIdRecyclingTest {
    private UserManagerService mUserManagerService;

    @Before
    public void setup() {
        // Currently UserManagerService cannot be instantiated twice inside a VM without a cleanup
        // TODO: Remove once UMS supports proper dependency injection
        if (Looper.myLooper() == null) {
            Looper.prepare();
        }
        LocalServices.removeServiceForTest(UserManagerInternal.class);
        mUserManagerService = new UserManagerService(InstrumentationRegistry.getContext());
    }

    @Test
    public void testUserCreateRecycleIdsAddAllThenRemove() {
        // Add max possible users
        for (int i = UserManagerService.MIN_USER_ID; i < UserManagerService.MAX_USER_ID; i++) {
            int userId = mUserManagerService.getNextAvailableId();
            assertEquals(i, userId);
            mUserManagerService.putUserInfo(newUserInfo(userId));
        }

        assertNoNextIdAvailable("All ids should be assigned");
        // Now remove RECENTLY_REMOVED_IDS_MAX_SIZE users in the middle
        int startFrom = UserManagerService.MIN_USER_ID + 10000 /* arbitrary number */;
        int lastId = startFrom + UserManagerService.MAX_RECENTLY_REMOVED_IDS_SIZE;
        for (int i = startFrom; i < lastId; i++) {
            removeUser(i);
            assertNoNextIdAvailable("There is not enough recently removed users. "
                    + "Next id should not be available. Failed at u" + i);
        }

        // Now remove first user
        removeUser(UserManagerService.MIN_USER_ID);

        // Released UserIDs should be returned in the FIFO order
        int nextId = mUserManagerService.getNextAvailableId();
        assertEquals(startFrom, nextId);
    }

    @Test
    public void testUserCreateRecycleIdsOverflow() {
        LinkedHashSet<Integer> queue = new LinkedHashSet<>();
        // Make sure we can generate more than 2x ids without issues
        for (int i = 0; i < UserManagerService.MAX_USER_ID * 2; i++) {
            int userId = mUserManagerService.getNextAvailableId();
            assertTrue("Returned id should not be recent. Id=" + userId + ". Recents=" + queue,
                    queue.add(userId));
            if (queue.size() > UserManagerService.MAX_RECENTLY_REMOVED_IDS_SIZE) {
                queue.remove(queue.iterator().next());
            }
            mUserManagerService.putUserInfo(newUserInfo(userId));
            removeUser(userId);
        }
    }

    private void removeUser(int userId) {
        mUserManagerService.removeUserInfo(userId);
        mUserManagerService.addRemovingUserIdLocked(userId);
    }

    private void assertNoNextIdAvailable(String message) {
        try {
            mUserManagerService.getNextAvailableId();
            fail(message);
        } catch (IllegalStateException e) {
            //OK
        }
    }

    private static UserInfo newUserInfo(int userId) {
        return new UserInfo(userId, "User " + userId, 0);
    }
}