diff options
| -rw-r--r-- | services/core/java/com/android/server/IpSecService.java | 219 | ||||
| -rw-r--r-- | tests/net/java/com/android/server/IpSecServiceRefcountedResourceTest.java | 356 |
2 files changed, 555 insertions, 20 deletions
diff --git a/services/core/java/com/android/server/IpSecService.java b/services/core/java/com/android/server/IpSecService.java index 1154fbe60973..92db7cc83405 100644 --- a/services/core/java/com/android/server/IpSecService.java +++ b/services/core/java/com/android/server/IpSecService.java @@ -57,11 +57,23 @@ import java.io.PrintWriter; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import libcore.io.IoUtils; -/** @hide */ +/** + * A service to manage multiple clients that want to access the IpSec API. The service is + * responsible for maintaining a list of clients and managing the resources (and related quotas) + * that each of them own. + * + * <p>Synchronization in IpSecService is done on all entrypoints due to potential race conditions at + * the kernel/xfrm level. Further, this allows the simplifying assumption to be made that only one + * thread is ever running at a time. + * + * @hide + */ public class IpSecService extends IIpSecService.Stub { private static final String TAG = "IpSecService"; private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); @@ -92,15 +104,15 @@ public class IpSecService extends IIpSecService.Stub { private static AtomicInteger mNextResourceId = new AtomicInteger(0x00FADED0); @GuardedBy("this") - private final ManagedResourceArray<SpiRecord> mSpiRecords = new ManagedResourceArray<>(); + private final KernelResourceArray<SpiRecord> mSpiRecords = new KernelResourceArray<>(); @GuardedBy("this") - private final ManagedResourceArray<TransformRecord> mTransformRecords = - new ManagedResourceArray<>(); + private final KernelResourceArray<TransformRecord> mTransformRecords = + new KernelResourceArray<>(); @GuardedBy("this") - private final ManagedResourceArray<UdpSocketRecord> mUdpSocketRecords = - new ManagedResourceArray<>(); + private final KernelResourceArray<UdpSocketRecord> mUdpSocketRecords = + new KernelResourceArray<>(); interface IpSecServiceConfiguration { INetd getNetdInstance() throws RemoteException; @@ -120,6 +132,173 @@ public class IpSecService extends IIpSecService.Stub { private final IpSecServiceConfiguration mSrvConfig; + /** + * Interface for user-reference and kernel-resource cleanup. + * + * <p>This interface must be implemented for a resource to be reference counted. + */ + @VisibleForTesting + public interface IResource { + /** + * Invalidates a IResource object, ensuring it is invalid for the purposes of allocating new + * objects dependent on it. + * + * <p>Implementations of this method are expected to remove references to the IResource + * object from the IpSecService's tracking arrays. The removal from the arrays ensures that + * the resource is considered invalid for user access or allocation or use in other + * resources. + * + * <p>References to the IResource object may be held by other RefcountedResource objects, + * and as such, the kernel resources and quota may not be cleaned up. + */ + void invalidate() throws RemoteException; + + /** + * Releases underlying resources and related quotas. + * + * <p>Implementations of this method are expected to remove all system resources that are + * tracked by the IResource object. Due to other RefcountedResource objects potentially + * having references to the IResource object, releaseKernelResources may not always be + * called from releaseIfUnreferencedRecursively(). + */ + void freeUnderlyingResources() throws RemoteException; + } + + /** + * RefcountedResource manages references and dependencies in an exclusively acyclic graph. + * + * <p>RefcountedResource implements both explicit and implicit resource management. Creating a + * RefcountedResource object creates an explicit reference that must be freed by calling + * userRelease(). Additionally, adding this object as a child of another RefcountedResource + * object will add an implicit reference. + * + * <p>Resources are cleaned up when all references, both implicit and explicit, are released + * (ie, when userRelease() is called and when all parents have called releaseReference() on this + * object.) + */ + @VisibleForTesting + public class RefcountedResource<T extends IResource> implements IBinder.DeathRecipient { + private final T mResource; + private final List<RefcountedResource> mChildren; + int mRefCount = 1; // starts at 1 for user's reference. + IBinder mBinder; + + RefcountedResource(T resource, IBinder binder, RefcountedResource... children) { + synchronized (IpSecService.this) { + this.mResource = resource; + this.mChildren = new ArrayList<>(children.length); + this.mBinder = binder; + + for (RefcountedResource child : children) { + mChildren.add(child); + child.mRefCount++; + } + + try { + mBinder.linkToDeath(this, 0); + } catch (RemoteException e) { + binderDied(); + } + } + } + + /** + * If the Binder object dies, this function is called to free the system resources that are + * being managed by this record and to subsequently release this record for garbage + * collection + */ + @Override + public void binderDied() { + synchronized (IpSecService.this) { + try { + userRelease(); + } catch (Exception e) { + Log.e(TAG, "Failed to release resource: " + e); + } + } + } + + public T getResource() { + return mResource; + } + + /** + * Unlinks from binder and performs IpSecService resource cleanup (removes from resource + * arrays) + * + * <p>If this method has been previously called, the RefcountedResource's binder field will + * be null, and the method will return without performing the cleanup a second time. + * + * <p>Note that calling this function does not imply that kernel resources will be freed at + * this time, or that the related quota will be returned. Such actions will only be + * performed upon the reference count reaching zero. + */ + @GuardedBy("IpSecService.this") + public void userRelease() throws RemoteException { + // Prevent users from putting reference counts into a bad state by calling + // userRelease() multiple times. + if (mBinder == null) { + return; + } + + mBinder.unlinkToDeath(this, 0); + mBinder = null; + + mResource.invalidate(); + + releaseReference(); + } + + /** + * Removes a reference to this resource. If the resultant reference count is zero, the + * underlying resources are freed, and references to all child resources are also dropped + * recursively (resulting in them freeing their resources and children, etcetera) + * + * <p>This method also sets the reference count to an invalid value (-1) to signify that it + * has been fully released. Any subsequent calls to this method will result in an + * IllegalStateException being thrown due to resource already having been previously + * released + */ + @VisibleForTesting + @GuardedBy("IpSecService.this") + public void releaseReference() throws RemoteException { + mRefCount--; + + if (mRefCount > 0) { + return; + } else if (mRefCount < 0) { + throw new IllegalStateException( + "Invalid operation - resource has already been released."); + } + + // Cleanup own resources + mResource.freeUnderlyingResources(); + + // Cleanup child resources as needed + for (RefcountedResource<? extends IResource> child : mChildren) { + child.releaseReference(); + } + + // Enforce that resource cleanup can only be called once + // By decrementing the refcount (from 0 to -1), the next call will throw an + // IllegalStateException - it has already been released fully. + mRefCount--; + } + + @Override + public String toString() { + return new StringBuilder() + .append("{mResource=") + .append(mResource) + .append(", mRefCount=") + .append(mRefCount) + .append(", mChildren=") + .append(mChildren) + .append("}") + .toString(); + } + } + /* Very simple counting class that looks much like a counting semaphore */ public static class ResourceTracker { private final int mMax; @@ -211,13 +390,13 @@ public class IpSecService extends IIpSecService.Stub { private final UserQuotaTracker mUserQuotaTracker = new UserQuotaTracker(); /** - * The ManagedResource class provides a facility to cleanly and reliably release system - * resources. It relies on two things: an IBinder that allows ManagedResource to automatically + * The KernelResource class provides a facility to cleanly and reliably release system + * resources. It relies on two things: an IBinder that allows KernelResource to automatically * clean up in the event that the Binder dies and a user-provided resourceId that should * uniquely identify the managed resource. To use this class, the user should implement the * releaseResources() method that is responsible for releasing system resources when invoked. */ - private abstract class ManagedResource implements IBinder.DeathRecipient { + private abstract class KernelResource implements IBinder.DeathRecipient { final int pid; final int uid; private IBinder mBinder; @@ -225,7 +404,7 @@ public class IpSecService extends IIpSecService.Stub { private AtomicInteger mReferenceCount = new AtomicInteger(0); - ManagedResource(int resourceId, IBinder binder) { + KernelResource(int resourceId, IBinder binder) { super(); if (resourceId == INVALID_RESOURCE_ID) { throw new IllegalArgumentException("Resource ID must not be INVALID_RESOURCE_ID"); @@ -341,7 +520,7 @@ public class IpSecService extends IIpSecService.Stub { /** * Minimal wrapper around SparseArray that performs ownership validation on element accesses. */ - private class ManagedResourceArray<T extends ManagedResource> { + private class KernelResourceArray<T extends KernelResource> { SparseArray<T> mArray = new SparseArray<>(); T getAndCheckOwner(int key) { @@ -369,7 +548,7 @@ public class IpSecService extends IIpSecService.Stub { } } - private final class TransformRecord extends ManagedResource { + private final class TransformRecord extends KernelResource { private final IpSecConfig mConfig; private final SpiRecord[] mSpis; private final UdpSocketRecord mSocket; @@ -456,7 +635,7 @@ public class IpSecService extends IIpSecService.Stub { } } - private final class SpiRecord extends ManagedResource { + private final class SpiRecord extends KernelResource { private final int mDirection; private final String mLocalAddress; private final String mRemoteAddress; @@ -544,7 +723,7 @@ public class IpSecService extends IIpSecService.Stub { } } - private final class UdpSocketRecord extends ManagedResource { + private final class UdpSocketRecord extends KernelResource { private FileDescriptor mSocket; private final int mPort; @@ -718,8 +897,8 @@ public class IpSecService extends IIpSecService.Stub { /* This method should only be called from Binder threads. Do not call this from * within the system server as it will crash the system on failure. */ - private synchronized <T extends ManagedResource> void releaseManagedResource( - ManagedResourceArray<T> resArray, int resourceId, String typeName) + private synchronized <T extends KernelResource> void releaseKernelResource( + KernelResourceArray<T> resArray, int resourceId, String typeName) throws RemoteException { // We want to non-destructively get so that we can check credentials before removing // this from the records. @@ -737,7 +916,7 @@ public class IpSecService extends IIpSecService.Stub { /** Release a previously allocated SPI that has been registered with the system server */ @Override public void releaseSecurityParameterIndex(int resourceId) throws RemoteException { - releaseManagedResource(mSpiRecords, resourceId, "SecurityParameterIndex"); + releaseKernelResource(mSpiRecords, resourceId, "SecurityParameterIndex"); } /** @@ -827,7 +1006,7 @@ public class IpSecService extends IIpSecService.Stub { @Override public void closeUdpEncapsulationSocket(int resourceId) throws RemoteException { - releaseManagedResource(mUdpSocketRecords, resourceId, "UdpEncapsulationSocket"); + releaseKernelResource(mUdpSocketRecords, resourceId, "UdpEncapsulationSocket"); } /** @@ -974,7 +1153,7 @@ public class IpSecService extends IIpSecService.Stub { */ @Override public void deleteTransportModeTransform(int resourceId) throws RemoteException { - releaseManagedResource(mTransformRecords, resourceId, "IpSecTransform"); + releaseKernelResource(mTransformRecords, resourceId, "IpSecTransform"); } /** @@ -984,7 +1163,7 @@ public class IpSecService extends IIpSecService.Stub { @Override public synchronized void applyTransportModeTransform( ParcelFileDescriptor socket, int resourceId) throws RemoteException { - // Synchronize liberally here because we are using ManagedResources in this block + // Synchronize liberally here because we are using KernelResources in this block TransformRecord info; // FIXME: this code should be factored out into a security check + getter info = mTransformRecords.getAndCheckOwner(resourceId); diff --git a/tests/net/java/com/android/server/IpSecServiceRefcountedResourceTest.java b/tests/net/java/com/android/server/IpSecServiceRefcountedResourceTest.java new file mode 100644 index 000000000000..cf8f715f23af --- /dev/null +++ b/tests/net/java/com/android/server/IpSecServiceRefcountedResourceTest.java @@ -0,0 +1,356 @@ +/* + * Copyright (C) 2017 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; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyObject; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.os.Binder; +import android.os.IBinder; +import android.os.RemoteException; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import com.android.server.IpSecService.IResource; +import com.android.server.IpSecService.RefcountedResource; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ThreadLocalRandom; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Unit tests for {@link IpSecService.RefcountedResource}. */ +@SmallTest +@RunWith(AndroidJUnit4.class) +public class IpSecServiceRefcountedResourceTest { + Context mMockContext; + IpSecService.IpSecServiceConfiguration mMockIpSecSrvConfig; + IpSecService mIpSecService; + + @Before + public void setUp() throws Exception { + mMockContext = mock(Context.class); + mMockIpSecSrvConfig = mock(IpSecService.IpSecServiceConfiguration.class); + mIpSecService = new IpSecService(mMockContext, mMockIpSecSrvConfig); + } + + private void assertResourceState( + RefcountedResource<IResource> resource, + int refCount, + int userReleaseCallCount, + int releaseReferenceCallCount, + int invalidateCallCount, + int freeUnderlyingResourcesCallCount) + throws RemoteException { + // Check refcount on RefcountedResource + assertEquals(refCount, resource.mRefCount); + + // Check call count of RefcountedResource + verify(resource, times(userReleaseCallCount)).userRelease(); + verify(resource, times(releaseReferenceCallCount)).releaseReference(); + + // Check call count of IResource + verify(resource.getResource(), times(invalidateCallCount)).invalidate(); + verify(resource.getResource(), times(freeUnderlyingResourcesCallCount)) + .freeUnderlyingResources(); + } + + /** Adds mockito instrumentation */ + private RefcountedResource<IResource> getTestRefcountedResource( + RefcountedResource... children) { + return getTestRefcountedResource(new Binder(), children); + } + + /** Adds mockito instrumentation with provided binder */ + private RefcountedResource<IResource> getTestRefcountedResource( + IBinder binder, RefcountedResource... children) { + return spy( + mIpSecService + .new RefcountedResource<IResource>(mock(IResource.class), binder, children)); + } + + @Test + public void testConstructor() throws RemoteException { + IBinder binderMock = mock(IBinder.class); + RefcountedResource<IResource> resource = getTestRefcountedResource(binderMock); + + // Verify resource's refcount starts at 1 (for user-reference) + assertResourceState(resource, 1, 0, 0, 0, 0); + + // Verify linking to binder death + verify(binderMock).linkToDeath(anyObject(), anyInt()); + } + + @Test + public void testConstructorWithChildren() throws RemoteException { + IBinder binderMockChild = mock(IBinder.class); + IBinder binderMockParent = mock(IBinder.class); + RefcountedResource<IResource> childResource = getTestRefcountedResource(binderMockChild); + RefcountedResource<IResource> parentResource = + getTestRefcountedResource(binderMockParent, childResource); + + // Verify parent's refcount starts at 1 (for user-reference) + assertResourceState(parentResource, 1, 0, 0, 0, 0); + + // Verify child's refcounts were incremented + assertResourceState(childResource, 2, 0, 0, 0, 0); + + // Verify linking to binder death + verify(binderMockChild).linkToDeath(anyObject(), anyInt()); + verify(binderMockParent).linkToDeath(anyObject(), anyInt()); + } + + @Test + public void testFailLinkToDeath() throws RemoteException { + IBinder binderMock = mock(IBinder.class); + doThrow(new RemoteException()).when(binderMock).linkToDeath(anyObject(), anyInt()); + + RefcountedResource<IResource> refcountedResource = getTestRefcountedResource(binderMock); + + // Verify that cleanup is performed (Spy limitations prevent verification of method calls + // for binder death scenario; check refcount to determine if cleanup was performed.) + assertEquals(-1, refcountedResource.mRefCount); + } + + @Test + public void testCleanupAndRelease() throws RemoteException { + IBinder binderMock = mock(IBinder.class); + RefcountedResource<IResource> refcountedResource = getTestRefcountedResource(binderMock); + + // Verify user-initiated cleanup path decrements refcount and calls full cleanup flow + refcountedResource.userRelease(); + assertResourceState(refcountedResource, -1, 1, 1, 1, 1); + + // Verify user-initated cleanup path unlinks from binder + verify(binderMock).unlinkToDeath(eq(refcountedResource), eq(0)); + assertNull(refcountedResource.mBinder); + } + + @Test + public void testMultipleCallsToCleanupAndRelease() throws RemoteException { + RefcountedResource<IResource> refcountedResource = getTestRefcountedResource(); + + // Verify calling userRelease multiple times does not trigger any other cleanup + // methods + refcountedResource.userRelease(); + assertResourceState(refcountedResource, -1, 1, 1, 1, 1); + + refcountedResource.userRelease(); + refcountedResource.userRelease(); + assertResourceState(refcountedResource, -1, 3, 1, 1, 1); + } + + @Test + public void testBinderDeathAfterCleanupAndReleaseDoesNothing() throws RemoteException { + RefcountedResource<IResource> refcountedResource = getTestRefcountedResource(); + + refcountedResource.userRelease(); + assertResourceState(refcountedResource, -1, 1, 1, 1, 1); + + // Verify binder death call does not trigger any other cleanup methods if called after + // userRelease() + refcountedResource.binderDied(); + assertResourceState(refcountedResource, -1, 2, 1, 1, 1); + } + + @Test + public void testBinderDeath() throws RemoteException { + RefcountedResource<IResource> refcountedResource = getTestRefcountedResource(); + + // Verify binder death caused cleanup + refcountedResource.binderDied(); + verify(refcountedResource, times(1)).binderDied(); + assertResourceState(refcountedResource, -1, 1, 1, 1, 1); + assertNull(refcountedResource.mBinder); + } + + @Test + public void testCleanupParentDecrementsChildRefcount() throws RemoteException { + RefcountedResource<IResource> childResource = getTestRefcountedResource(); + RefcountedResource<IResource> parentResource = getTestRefcountedResource(childResource); + + parentResource.userRelease(); + + // Verify parent gets cleaned up properly, and triggers releaseReference on + // child + assertResourceState(childResource, 1, 0, 1, 0, 0); + assertResourceState(parentResource, -1, 1, 1, 1, 1); + } + + @Test + public void testCleanupReferencedChildDoesNotTriggerRelease() throws RemoteException { + RefcountedResource<IResource> childResource = getTestRefcountedResource(); + RefcountedResource<IResource> parentResource = getTestRefcountedResource(childResource); + + childResource.userRelease(); + + // Verify that child does not clean up kernel resources and quota. + assertResourceState(childResource, 1, 1, 1, 1, 0); + assertResourceState(parentResource, 1, 0, 0, 0, 0); + } + + @Test + public void testTwoParents() throws RemoteException { + RefcountedResource<IResource> childResource = getTestRefcountedResource(); + RefcountedResource<IResource> parentResource1 = getTestRefcountedResource(childResource); + RefcountedResource<IResource> parentResource2 = getTestRefcountedResource(childResource); + + // Verify that child does not cleanup kernel resources and quota until all references + // have been released. Assumption: parents release correctly based on + // testCleanupParentDecrementsChildRefcount() + childResource.userRelease(); + assertResourceState(childResource, 2, 1, 1, 1, 0); + + parentResource1.userRelease(); + assertResourceState(childResource, 1, 1, 2, 1, 0); + + parentResource2.userRelease(); + assertResourceState(childResource, -1, 1, 3, 1, 1); + } + + @Test + public void testTwoChildren() throws RemoteException { + RefcountedResource<IResource> childResource1 = getTestRefcountedResource(); + RefcountedResource<IResource> childResource2 = getTestRefcountedResource(); + RefcountedResource<IResource> parentResource = + getTestRefcountedResource(childResource1, childResource2); + + childResource1.userRelease(); + assertResourceState(childResource1, 1, 1, 1, 1, 0); + assertResourceState(childResource2, 2, 0, 0, 0, 0); + + parentResource.userRelease(); + assertResourceState(childResource1, -1, 1, 2, 1, 1); + assertResourceState(childResource2, 1, 0, 1, 0, 0); + + childResource2.userRelease(); + assertResourceState(childResource1, -1, 1, 2, 1, 1); + assertResourceState(childResource2, -1, 1, 2, 1, 1); + } + + @Test + public void testSampleUdpEncapTranform() throws RemoteException { + RefcountedResource<IResource> spi1 = getTestRefcountedResource(); + RefcountedResource<IResource> spi2 = getTestRefcountedResource(); + RefcountedResource<IResource> udpEncapSocket = getTestRefcountedResource(); + RefcountedResource<IResource> transform = + getTestRefcountedResource(spi1, spi2, udpEncapSocket); + + // Pretend one SPI goes out of reference (releaseManagedResource -> userRelease) + spi1.userRelease(); + + // User called releaseManagedResource on udpEncap socket + udpEncapSocket.userRelease(); + + // User dies, and binder kills the rest + spi2.binderDied(); + transform.binderDied(); + + // Check resource states + assertResourceState(spi1, -1, 1, 2, 1, 1); + assertResourceState(spi2, -1, 1, 2, 1, 1); + assertResourceState(udpEncapSocket, -1, 1, 2, 1, 1); + assertResourceState(transform, -1, 1, 1, 1, 1); + } + + @Test + public void testSampleDualTransformEncapSocket() throws RemoteException { + RefcountedResource<IResource> spi1 = getTestRefcountedResource(); + RefcountedResource<IResource> spi2 = getTestRefcountedResource(); + RefcountedResource<IResource> spi3 = getTestRefcountedResource(); + RefcountedResource<IResource> spi4 = getTestRefcountedResource(); + RefcountedResource<IResource> udpEncapSocket = getTestRefcountedResource(); + RefcountedResource<IResource> transform1 = + getTestRefcountedResource(spi1, spi2, udpEncapSocket); + RefcountedResource<IResource> transform2 = + getTestRefcountedResource(spi3, spi4, udpEncapSocket); + + // Pretend one SPIs goes out of reference (releaseManagedResource -> userRelease) + spi1.userRelease(); + + // User called releaseManagedResource on udpEncap socket and spi4 + udpEncapSocket.userRelease(); + spi4.userRelease(); + + // User dies, and binder kills the rest + spi2.binderDied(); + spi3.binderDied(); + transform2.binderDied(); + transform1.binderDied(); + + // Check resource states + assertResourceState(spi1, -1, 1, 2, 1, 1); + assertResourceState(spi2, -1, 1, 2, 1, 1); + assertResourceState(spi3, -1, 1, 2, 1, 1); + assertResourceState(spi4, -1, 1, 2, 1, 1); + assertResourceState(udpEncapSocket, -1, 1, 3, 1, 1); + assertResourceState(transform1, -1, 1, 1, 1, 1); + assertResourceState(transform2, -1, 1, 1, 1, 1); + } + + @Test + public void fuzzTest() throws RemoteException { + List<RefcountedResource<IResource>> resources = new ArrayList<>(); + + // Build a tree of resources + for (int i = 0; i < 100; i++) { + // Choose a random number of children from the existing list + int numChildren = ThreadLocalRandom.current().nextInt(0, resources.size() + 1); + + // Build a (random) list of children + Set<RefcountedResource<IResource>> children = new HashSet<>(); + for (int j = 0; j < numChildren; j++) { + int childIndex = ThreadLocalRandom.current().nextInt(0, resources.size()); + children.add(resources.get(childIndex)); + } + + RefcountedResource<IResource> newRefcountedResource = + getTestRefcountedResource( + children.toArray(new RefcountedResource[children.size()])); + resources.add(newRefcountedResource); + } + + // Cleanup all resources in a random order + List<RefcountedResource<IResource>> clonedResources = + new ArrayList<>(resources); // shallow copy + while (!clonedResources.isEmpty()) { + int index = ThreadLocalRandom.current().nextInt(0, clonedResources.size()); + RefcountedResource<IResource> refcountedResource = clonedResources.get(index); + refcountedResource.userRelease(); + clonedResources.remove(index); + } + + // Verify all resources were cleaned up properly + for (RefcountedResource<IResource> refcountedResource : resources) { + assertEquals(-1, refcountedResource.mRefCount); + } + } +} |