Convert IpSecService resources to use refcounting

This is part 2 of 2 of the refcounting refactor for IpSecService
resources.

Switched ManagedResources to use RefcountedResource structure for
managing reference counts and eventual cleanup. Further, resource arrays
and quota management have been aggregated into a UserRecord for better
isolation. UID access checking has been similarly moved into the
UserRecordTracker, and resourceId checking has been rolled into
RefcountedResourceArray's accessor methods.

Bug: 63409385
Test: CTS, all unit tests run on aosp_marlin-eng, new tests added
Change-Id: Iee52dd1c9d2583bb6bfaf65be87569e9d50a5b63
diff --git a/services/core/java/com/android/server/IpSecService.java b/services/core/java/com/android/server/IpSecService.java
index 92db7cc..2116d9a 100644
--- a/services/core/java/com/android/server/IpSecService.java
+++ b/services/core/java/com/android/server/IpSecService.java
@@ -103,17 +103,6 @@
     /** Should be a never-repeating global ID for resources */
     private static AtomicInteger mNextResourceId = new AtomicInteger(0x00FADED0);
 
-    @GuardedBy("this")
-    private final KernelResourceArray<SpiRecord> mSpiRecords = new KernelResourceArray<>();
-
-    @GuardedBy("this")
-    private final KernelResourceArray<TransformRecord> mTransformRecords =
-            new KernelResourceArray<>();
-
-    @GuardedBy("this")
-    private final KernelResourceArray<UdpSocketRecord> mUdpSocketRecords =
-            new KernelResourceArray<>();
-
     interface IpSecServiceConfiguration {
         INetd getNetdInstance() throws RemoteException;
 
@@ -158,7 +147,7 @@
          *
          * <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
+         * having references to the IResource object, freeUnderlyingResources may not always be
          * called from releaseIfUnreferencedRecursively().
          */
         void freeUnderlyingResources() throws RemoteException;
@@ -204,7 +193,7 @@
 
         /**
          * 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
+         * being tracked by this record and to subsequently release this record for garbage
          * collection
          */
         @Override
@@ -300,7 +289,8 @@
     }
 
     /* Very simple counting class that looks much like a counting semaphore */
-    public static class ResourceTracker {
+    @VisibleForTesting
+    static class ResourceTracker {
         private final int mMax;
         int mCurrent;
 
@@ -309,18 +299,18 @@
             mCurrent = 0;
         }
 
-        synchronized boolean isAvailable() {
+        boolean isAvailable() {
             return (mCurrent < mMax);
         }
 
-        synchronized void take() {
+        void take() {
             if (!isAvailable()) {
                 Log.wtf(TAG, "Too many resources allocated!");
             }
             mCurrent++;
         }
 
-        synchronized void give() {
+        void give() {
             if (mCurrent <= 0) {
                 Log.wtf(TAG, "We've released this resource too many times");
             }
@@ -339,40 +329,70 @@
         }
     }
 
-    private static final class UserQuotaTracker {
-        /* Maximum number of UDP Encap Sockets that a single UID may possess */
+    @VisibleForTesting
+    static final class UserRecord {
+        /* Type names */
+        public static final String TYPENAME_SPI = "SecurityParameterIndex";
+        public static final String TYPENAME_TRANSFORM = "IpSecTransform";
+        public static final String TYPENAME_ENCAP_SOCKET = "UdpEncapSocket";
+
+        /* Maximum number of each type of resource that a single UID may possess */
         public static final int MAX_NUM_ENCAP_SOCKETS = 2;
-
-        /* Maximum number of IPsec Transforms that a single UID may possess */
         public static final int MAX_NUM_TRANSFORMS = 4;
-
-        /* Maximum number of IPsec Transforms that a single UID may possess */
         public static final int MAX_NUM_SPIS = 8;
 
-        /* Record for one users's IpSecService-managed objects */
-        public static class UserRecord {
-            public final ResourceTracker socket = new ResourceTracker(MAX_NUM_ENCAP_SOCKETS);
-            public final ResourceTracker transform = new ResourceTracker(MAX_NUM_TRANSFORMS);
-            public final ResourceTracker spi = new ResourceTracker(MAX_NUM_SPIS);
+        final RefcountedResourceArray<SpiRecord> mSpiRecords =
+                new RefcountedResourceArray<>(TYPENAME_SPI);
+        final ResourceTracker mSpiQuotaTracker = new ResourceTracker(MAX_NUM_SPIS);
 
-            @Override
-            public String toString() {
-                return new StringBuilder()
-                        .append("{socket=")
-                        .append(socket)
-                        .append(", transform=")
-                        .append(transform)
-                        .append(", spi=")
-                        .append(spi)
-                        .append("}")
-                        .toString();
-            }
+        final RefcountedResourceArray<TransformRecord> mTransformRecords =
+                new RefcountedResourceArray<>(TYPENAME_TRANSFORM);
+        final ResourceTracker mTransformQuotaTracker = new ResourceTracker(MAX_NUM_TRANSFORMS);
+
+        final RefcountedResourceArray<EncapSocketRecord> mEncapSocketRecords =
+                new RefcountedResourceArray<>(TYPENAME_ENCAP_SOCKET);
+        final ResourceTracker mSocketQuotaTracker = new ResourceTracker(MAX_NUM_ENCAP_SOCKETS);
+
+        void removeSpiRecord(int resourceId) {
+            mSpiRecords.remove(resourceId);
         }
 
+        void removeTransformRecord(int resourceId) {
+            mTransformRecords.remove(resourceId);
+        }
+
+        void removeEncapSocketRecord(int resourceId) {
+            mEncapSocketRecords.remove(resourceId);
+        }
+
+        @Override
+        public String toString() {
+            return new StringBuilder()
+                    .append("{mSpiQuotaTracker=")
+                    .append(mSpiQuotaTracker)
+                    .append(", mTransformQuotaTracker=")
+                    .append(mTransformQuotaTracker)
+                    .append(", mSocketQuotaTracker=")
+                    .append(mSocketQuotaTracker)
+                    .append(", mSpiRecords=")
+                    .append(mSpiRecords)
+                    .append(", mTransformRecords=")
+                    .append(mTransformRecords)
+                    .append(", mEncapSocketRecords=")
+                    .append(mEncapSocketRecords)
+                    .append("}")
+                    .toString();
+        }
+    }
+
+    @VisibleForTesting
+    static final class UserResourceTracker {
         private final SparseArray<UserRecord> mUserRecords = new SparseArray<>();
 
-        /* a never-fail getter so that we can populate the list of UIDs as-needed */
-        public synchronized UserRecord getUserRecord(int uid) {
+        /** Never-fail getter that populates the list of UIDs as-needed */
+        public UserRecord getUserRecord(int uid) {
+            checkCallerUid(uid);
+
             UserRecord r = mUserRecords.get(uid);
             if (r == null) {
                 r = new UserRecord();
@@ -381,122 +401,56 @@
             return r;
         }
 
+        /** Safety method; guards against access of other user's UserRecords */
+        private void checkCallerUid(int uid) {
+            if (uid != Binder.getCallingUid()
+                    && android.os.Process.SYSTEM_UID != Binder.getCallingUid()) {
+                throw new SecurityException("Attempted access of unowned resources");
+            }
+        }
+
         @Override
         public String toString() {
             return mUserRecords.toString();
         }
     }
 
-    private final UserQuotaTracker mUserQuotaTracker = new UserQuotaTracker();
+    @VisibleForTesting final UserResourceTracker mUserResourceTracker = new UserResourceTracker();
 
     /**
-     * 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.
+     * The KernelResourceRecord class provides a facility to cleanly and reliably track system
+     * resources. It relies on a provided resourceId that should uniquely identify the kernel
+     * resource. To use this class, the user should implement the invalidate() and
+     * freeUnderlyingResources() methods that are responsible for cleaning up IpSecService resource
+     * tracking arrays and kernel resources, respectively
      */
-    private abstract class KernelResource implements IBinder.DeathRecipient {
+    private abstract class KernelResourceRecord implements IResource {
         final int pid;
         final int uid;
-        private IBinder mBinder;
-        protected int mResourceId;
+        protected final int mResourceId;
 
-        private AtomicInteger mReferenceCount = new AtomicInteger(0);
-
-        KernelResource(int resourceId, IBinder binder) {
+        KernelResourceRecord(int resourceId) {
             super();
             if (resourceId == INVALID_RESOURCE_ID) {
                 throw new IllegalArgumentException("Resource ID must not be INVALID_RESOURCE_ID");
             }
-            mBinder = binder;
             mResourceId = resourceId;
             pid = Binder.getCallingPid();
             uid = Binder.getCallingUid();
 
             getResourceTracker().take();
-            try {
-                mBinder.linkToDeath(this, 0);
-            } catch (RemoteException e) {
-                binderDied();
-            }
         }
 
-        public void addReference() {
-            mReferenceCount.incrementAndGet();
+        @Override
+        public abstract void invalidate() throws RemoteException;
+
+        /** Convenience method; retrieves the user resource record for the stored UID. */
+        protected UserRecord getUserRecord() {
+            return mUserResourceTracker.getUserRecord(uid);
         }
 
-        public void removeReference() {
-            if (mReferenceCount.decrementAndGet() < 0) {
-                Log.wtf(TAG, "Programming error: negative reference count");
-            }
-        }
-
-        public boolean isReferenced() {
-            return (mReferenceCount.get() > 0);
-        }
-
-        /**
-         * Ensures that the caller is either the owner of this resource or has the system UID and
-         * throws a SecurityException otherwise.
-         */
-        public void checkOwnerOrSystem() {
-            if (uid != Binder.getCallingUid()
-                    && android.os.Process.SYSTEM_UID != Binder.getCallingUid()) {
-                throw new SecurityException("Only the owner may access managed resources!");
-            }
-        }
-
-        /**
-         * When this record is no longer needed for managing system resources this function should
-         * clean up all system resources and nullify the record. This function shall perform all
-         * necessary cleanup of the resources managed by this record.
-         *
-         * <p>NOTE: this function verifies ownership before allowing resources to be freed.
-         */
-        public final void release() throws RemoteException {
-            synchronized (IpSecService.this) {
-                if (isReferenced()) {
-                    throw new IllegalStateException(
-                            "Cannot release a resource that has active references!");
-                }
-
-                if (mResourceId == INVALID_RESOURCE_ID) {
-                    return;
-                }
-
-                releaseResources();
-                getResourceTracker().give();
-                if (mBinder != null) {
-                    mBinder.unlinkToDeath(this, 0);
-                }
-                mBinder = null;
-
-                mResourceId = INVALID_RESOURCE_ID;
-            }
-        }
-
-        /**
-         * 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
-         */
-        public final void binderDied() {
-            try {
-                release();
-            } catch (Exception e) {
-                Log.e(TAG, "Failed to release resource: " + e);
-            }
-        }
-
-        /**
-         * Implement this method to release all system resources that are being protected by this
-         * record. Once the resources are released, the record should be invalidated and no longer
-         * used by calling release(). This should NEVER be called directly.
-         *
-         * <p>Calls to this are always guarded by IpSecService#this
-         */
-        protected abstract void releaseResources() throws RemoteException;
+        @Override
+        public abstract void freeUnderlyingResources() throws RemoteException;
 
         /** Get the resource tracker for this resource */
         protected abstract ResourceTracker getResourceTracker();
@@ -510,30 +464,52 @@
                     .append(pid)
                     .append(", uid=")
                     .append(uid)
-                    .append(", mReferenceCount=")
-                    .append(mReferenceCount.get())
                     .append("}")
                     .toString();
         }
     };
 
+    // TODO: Move this to right after RefcountedResource. With this here, Gerrit was showing many
+    // more things as changed.
     /**
-     * Minimal wrapper around SparseArray that performs ownership validation on element accesses.
+     * Thin wrapper over SparseArray to ensure resources exist, and simplify generic typing.
+     *
+     * <p>RefcountedResourceArray prevents null insertions, and throws an IllegalArgumentException
+     * if a key is not found during a retrieval process.
      */
-    private class KernelResourceArray<T extends KernelResource> {
-        SparseArray<T> mArray = new SparseArray<>();
+    static class RefcountedResourceArray<T extends IResource> {
+        SparseArray<RefcountedResource<T>> mArray = new SparseArray<>();
+        private final String mTypeName;
 
-        T getAndCheckOwner(int key) {
-            T val = mArray.get(key);
-            // The value should never be null unless the resource doesn't exist
-            // (since we do not allow null resources to be added).
-            if (val != null) {
-                val.checkOwnerOrSystem();
-            }
-            return val;
+        public RefcountedResourceArray(String typeName) {
+            this.mTypeName = typeName;
         }
 
-        void put(int key, T obj) {
+        /**
+         * Accessor method to get inner resource object.
+         *
+         * @throws IllegalArgumentException if no resource with provided key is found.
+         */
+        T getResourceOrThrow(int key) {
+            return getRefcountedResourceOrThrow(key).getResource();
+        }
+
+        /**
+         * Accessor method to get reference counting wrapper.
+         *
+         * @throws IllegalArgumentException if no resource with provided key is found.
+         */
+        RefcountedResource<T> getRefcountedResourceOrThrow(int key) {
+            RefcountedResource<T> resource = mArray.get(key);
+            if (resource == null) {
+                throw new IllegalArgumentException(
+                        String.format("No such %s found for given id: %d", mTypeName, key));
+            }
+
+            return resource;
+        }
+
+        void put(int key, RefcountedResource<T> obj) {
             checkNotNull(obj, "Null resources cannot be added");
             mArray.put(key, obj);
         }
@@ -548,30 +524,17 @@
         }
     }
 
-    private final class TransformRecord extends KernelResource {
+    private final class TransformRecord extends KernelResourceRecord {
         private final IpSecConfig mConfig;
         private final SpiRecord[] mSpis;
-        private final UdpSocketRecord mSocket;
+        private final EncapSocketRecord mSocket;
 
         TransformRecord(
-                int resourceId,
-                IBinder binder,
-                IpSecConfig config,
-                SpiRecord[] spis,
-                UdpSocketRecord socket) {
-            super(resourceId, binder);
+                int resourceId, IpSecConfig config, SpiRecord[] spis, EncapSocketRecord socket) {
+            super(resourceId);
             mConfig = config;
             mSpis = spis;
             mSocket = socket;
-
-            for (int direction : DIRECTIONS) {
-                mSpis[direction].addReference();
-                mSpis[direction].setOwnedByTransform();
-            }
-
-            if (mSocket != null) {
-                mSocket.addReference();
-            }
         }
 
         public IpSecConfig getConfig() {
@@ -584,7 +547,7 @@
 
         /** always guarded by IpSecService#this */
         @Override
-        protected void releaseResources() {
+        public void freeUnderlyingResources() {
             for (int direction : DIRECTIONS) {
                 int spi = mSpis[direction].getSpi();
                 try {
@@ -603,17 +566,17 @@
                 }
             }
 
-            for (int direction : DIRECTIONS) {
-                mSpis[direction].removeReference();
-            }
-
-            if (mSocket != null) {
-                mSocket.removeReference();
-            }
+            getResourceTracker().give();
         }
 
+        @Override
+        public void invalidate() throws RemoteException {
+            getUserRecord().removeTransformRecord(mResourceId);
+        }
+
+        @Override
         protected ResourceTracker getResourceTracker() {
-            return mUserQuotaTracker.getUserRecord(this.uid).transform;
+            return getUserRecord().mTransformQuotaTracker;
         }
 
         @Override
@@ -635,7 +598,7 @@
         }
     }
 
-    private final class SpiRecord extends KernelResource {
+    private final class SpiRecord extends KernelResourceRecord {
         private final int mDirection;
         private final String mLocalAddress;
         private final String mRemoteAddress;
@@ -645,12 +608,11 @@
 
         SpiRecord(
                 int resourceId,
-                IBinder binder,
                 int direction,
                 String localAddress,
                 String remoteAddress,
                 int spi) {
-            super(resourceId, binder);
+            super(resourceId);
             mDirection = direction;
             mLocalAddress = localAddress;
             mRemoteAddress = remoteAddress;
@@ -659,7 +621,7 @@
 
         /** always guarded by IpSecService#this */
         @Override
-        protected void releaseResources() {
+        public void freeUnderlyingResources() {
             if (mOwnedByTransform) {
                 Log.d(TAG, "Cannot release Spi " + mSpi + ": Currently locked by a Transform");
                 // Because SPIs are "handed off" to transform, objects, they should never be
@@ -682,11 +644,8 @@
             }
 
             mSpi = IpSecManager.INVALID_SECURITY_PARAMETER_INDEX;
-        }
 
-        @Override
-        protected ResourceTracker getResourceTracker() {
-            return mUserQuotaTracker.getUserRecord(this.uid).spi;
+            getResourceTracker().give();
         }
 
         public int getSpi() {
@@ -703,6 +662,16 @@
         }
 
         @Override
+        public void invalidate() throws RemoteException {
+            getUserRecord().removeSpiRecord(mResourceId);
+        }
+
+        @Override
+        protected ResourceTracker getResourceTracker() {
+            return getUserRecord().mSpiQuotaTracker;
+        }
+
+        @Override
         public String toString() {
             StringBuilder strBuilder = new StringBuilder();
             strBuilder
@@ -723,27 +692,24 @@
         }
     }
 
-    private final class UdpSocketRecord extends KernelResource {
+    private final class EncapSocketRecord extends KernelResourceRecord {
         private FileDescriptor mSocket;
         private final int mPort;
 
-        UdpSocketRecord(int resourceId, IBinder binder, FileDescriptor socket, int port) {
-            super(resourceId, binder);
+        EncapSocketRecord(int resourceId, FileDescriptor socket, int port) {
+            super(resourceId);
             mSocket = socket;
             mPort = port;
         }
 
         /** always guarded by IpSecService#this */
         @Override
-        protected void releaseResources() {
+        public void freeUnderlyingResources() {
             Log.d(TAG, "Closing port " + mPort);
             IoUtils.closeQuietly(mSocket);
             mSocket = null;
-        }
 
-        @Override
-        protected ResourceTracker getResourceTracker() {
-            return mUserQuotaTracker.getUserRecord(this.uid).socket;
+            getResourceTracker().give();
         }
 
         public int getPort() {
@@ -755,6 +721,16 @@
         }
 
         @Override
+        protected ResourceTracker getResourceTracker() {
+            return getUserRecord().mSocketQuotaTracker;
+        }
+
+        @Override
+        public void invalidate() {
+            getUserRecord().removeEncapSocketRecord(mResourceId);
+        }
+
+        @Override
         public String toString() {
             return new StringBuilder()
                     .append("{super=")
@@ -861,13 +837,14 @@
         /* requestedSpi can be anything in the int range, so no check is needed. */
         checkNotNull(binder, "Null Binder passed to reserveSecurityParameterIndex");
 
+        UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
         int resourceId = mNextResourceId.getAndIncrement();
 
         int spi = IpSecManager.INVALID_SECURITY_PARAMETER_INDEX;
         String localAddress = "";
 
         try {
-            if (!mUserQuotaTracker.getUserRecord(Binder.getCallingUid()).spi.isAvailable()) {
+            if (!userRecord.mSpiQuotaTracker.isAvailable()) {
                 return new IpSecSpiResponse(
                         IpSecManager.Status.RESOURCE_UNAVAILABLE, INVALID_RESOURCE_ID, spi);
             }
@@ -881,9 +858,11 @@
                                     remoteAddress,
                                     requestedSpi);
             Log.d(TAG, "Allocated SPI " + spi);
-            mSpiRecords.put(
+            userRecord.mSpiRecords.put(
                     resourceId,
-                    new SpiRecord(resourceId, binder, direction, localAddress, remoteAddress, spi));
+                    new RefcountedResource<SpiRecord>(
+                            new SpiRecord(resourceId, direction, localAddress, remoteAddress, spi),
+                            binder));
         } catch (ServiceSpecificException e) {
             // TODO: Add appropriate checks when other ServiceSpecificException types are supported
             return new IpSecSpiResponse(
@@ -897,26 +876,17 @@
     /* 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 KernelResource> void releaseKernelResource(
-            KernelResourceArray<T> resArray, int resourceId, String typeName)
+    private void releaseResource(RefcountedResourceArray resArray, int resourceId)
             throws RemoteException {
-        // We want to non-destructively get so that we can check credentials before removing
-        // this from the records.
-        T record = resArray.getAndCheckOwner(resourceId);
 
-        if (record == null) {
-            throw new IllegalArgumentException(
-                    typeName + " " + resourceId + " is not available to be deleted");
-        }
-
-        record.release();
-        resArray.remove(resourceId);
+        resArray.getRefcountedResourceOrThrow(resourceId).userRelease();
     }
 
     /** Release a previously allocated SPI that has been registered with the system server */
     @Override
-    public void releaseSecurityParameterIndex(int resourceId) throws RemoteException {
-        releaseKernelResource(mSpiRecords, resourceId, "SecurityParameterIndex");
+    public synchronized void releaseSecurityParameterIndex(int resourceId) throws RemoteException {
+        UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
+        releaseResource(userRecord.mSpiRecords, resourceId);
     }
 
     /**
@@ -969,10 +939,11 @@
         }
         checkNotNull(binder, "Null Binder passed to openUdpEncapsulationSocket");
 
+        UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
         int resourceId = mNextResourceId.getAndIncrement();
         FileDescriptor sockFd = null;
         try {
-            if (!mUserQuotaTracker.getUserRecord(Binder.getCallingUid()).socket.isAvailable()) {
+            if (!userRecord.mSocketQuotaTracker.isAvailable()) {
                 return new IpSecUdpEncapResponse(IpSecManager.Status.RESOURCE_UNAVAILABLE);
             }
 
@@ -991,8 +962,10 @@
                     OsConstants.UDP_ENCAP,
                     OsConstants.UDP_ENCAP_ESPINUDP);
 
-            mUdpSocketRecords.put(
-                    resourceId, new UdpSocketRecord(resourceId, binder, sockFd, port));
+            userRecord.mEncapSocketRecords.put(
+                    resourceId,
+                    new RefcountedResource<EncapSocketRecord>(
+                            new EncapSocketRecord(resourceId, sockFd, port), binder));
             return new IpSecUdpEncapResponse(IpSecManager.Status.OK, resourceId, port, sockFd);
         } catch (IOException | ErrnoException e) {
             IoUtils.closeQuietly(sockFd);
@@ -1004,9 +977,9 @@
 
     /** close a socket that has been been allocated by and registered with the system server */
     @Override
-    public void closeUdpEncapsulationSocket(int resourceId) throws RemoteException {
-
-        releaseKernelResource(mUdpSocketRecords, resourceId, "UdpEncapsulationSocket");
+    public synchronized void closeUdpEncapsulationSocket(int resourceId) throws RemoteException {
+        UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
+        releaseResource(userRecord.mEncapSocketRecords, resourceId);
     }
 
     /**
@@ -1014,6 +987,8 @@
      * IllegalArgumentException if they are not.
      */
     private void checkIpSecConfig(IpSecConfig config) {
+        UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
+
         if (config.getLocalAddress() == null) {
             throw new IllegalArgumentException("Invalid null Local InetAddress");
         }
@@ -1042,12 +1017,9 @@
                 break;
             case IpSecTransform.ENCAP_ESPINUDP:
             case IpSecTransform.ENCAP_ESPINUDP_NON_IKE:
-                if (mUdpSocketRecords.getAndCheckOwner(
-                            config.getEncapSocketResourceId()) == null) {
-                    throw new IllegalStateException(
-                            "No Encapsulation socket for Resource Id: "
-                                    + config.getEncapSocketResourceId());
-                }
+                // Retrieve encap socket record; will throw IllegalArgumentException if not found
+                userRecord.mEncapSocketRecords.getResourceOrThrow(
+                        config.getEncapSocketResourceId());
 
                 int port = config.getEncapRemotePort();
                 if (port <= 0 || port > 0xFFFF) {
@@ -1071,9 +1043,8 @@
                                 + " exclusive with other Authentication or Encryption algorithms");
             }
 
-            if (mSpiRecords.getAndCheckOwner(config.getSpiResourceId(direction)) == null) {
-                throw new IllegalStateException("No SPI for specified Resource Id");
-            }
+            // Retrieve SPI record; will throw IllegalArgumentException if not found
+            userRecord.mSpiRecords.getResourceOrThrow(config.getSpiResourceId(direction));
         }
     }
 
@@ -1090,16 +1061,27 @@
         checkIpSecConfig(c);
         checkNotNull(binder, "Null Binder passed to createTransportModeTransform");
         int resourceId = mNextResourceId.getAndIncrement();
-        if (!mUserQuotaTracker.getUserRecord(Binder.getCallingUid()).transform.isAvailable()) {
+
+        UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
+
+        // Avoid resizing by creating a dependency array of min-size 3 (1 UDP encap + 2 SPIs)
+        List<RefcountedResource> dependencies = new ArrayList<>(3);
+
+        if (!userRecord.mTransformQuotaTracker.isAvailable()) {
             return new IpSecTransformResponse(IpSecManager.Status.RESOURCE_UNAVAILABLE);
         }
         SpiRecord[] spis = new SpiRecord[DIRECTIONS.length];
 
         int encapType, encapLocalPort = 0, encapRemotePort = 0;
-        UdpSocketRecord socketRecord = null;
+        EncapSocketRecord socketRecord = null;
         encapType = c.getEncapType();
         if (encapType != IpSecTransform.ENCAP_NONE) {
-            socketRecord = mUdpSocketRecords.getAndCheckOwner(c.getEncapSocketResourceId());
+            RefcountedResource<EncapSocketRecord> refcountedSocketRecord =
+                    userRecord.mEncapSocketRecords.getRefcountedResourceOrThrow(
+                            c.getEncapSocketResourceId());
+            dependencies.add(refcountedSocketRecord);
+
+            socketRecord = refcountedSocketRecord.getResource();
             encapLocalPort = socketRecord.getPort();
             encapRemotePort = c.getEncapRemotePort();
         }
@@ -1109,7 +1091,12 @@
             IpSecAlgorithm crypt = c.getEncryption(direction);
             IpSecAlgorithm authCrypt = c.getAuthenticatedEncryption(direction);
 
-            spis[direction] = mSpiRecords.getAndCheckOwner(c.getSpiResourceId(direction));
+            RefcountedResource<SpiRecord> refcountedSpiRecord =
+                    userRecord.mSpiRecords.getRefcountedResourceOrThrow(
+                            c.getSpiResourceId(direction));
+            dependencies.add(refcountedSpiRecord);
+
+            spis[direction] = refcountedSpiRecord.getResource();
             int spi = spis[direction].getSpi();
             try {
                 mSrvConfig
@@ -1140,8 +1127,12 @@
             }
         }
         // Both SAs were created successfully, time to construct a record and lock it away
-        mTransformRecords.put(
-                resourceId, new TransformRecord(resourceId, binder, c, spis, socketRecord));
+        userRecord.mTransformRecords.put(
+                resourceId,
+                new RefcountedResource<TransformRecord>(
+                        new TransformRecord(resourceId, c, spis, socketRecord),
+                        binder,
+                        dependencies.toArray(new RefcountedResource[dependencies.size()])));
         return new IpSecTransformResponse(IpSecManager.Status.OK, resourceId);
     }
 
@@ -1152,8 +1143,9 @@
      * other reasons.
      */
     @Override
-    public void deleteTransportModeTransform(int resourceId) throws RemoteException {
-        releaseKernelResource(mTransformRecords, resourceId, "IpSecTransform");
+    public synchronized void deleteTransportModeTransform(int resourceId) throws RemoteException {
+        UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
+        releaseResource(userRecord.mTransformRecords, resourceId);
     }
 
     /**
@@ -1163,14 +1155,10 @@
     @Override
     public synchronized void applyTransportModeTransform(
             ParcelFileDescriptor socket, int resourceId) throws RemoteException {
-        // 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);
+        UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
 
-        if (info == null) {
-            throw new IllegalArgumentException("Transform " + resourceId + " is not active");
-        }
+        // Get transform record; if no transform is found, will throw IllegalArgumentException
+        TransformRecord info = userRecord.mTransformRecords.getResourceOrThrow(resourceId);
 
         // TODO: make this a function.
         if (info.pid != getCallingPid() || info.uid != getCallingUid()) {
@@ -1202,7 +1190,7 @@
      * used: reserved for future improved input validation.
      */
     @Override
-    public void removeTransportModeTransform(ParcelFileDescriptor socket, int resourceId)
+    public synchronized void removeTransportModeTransform(ParcelFileDescriptor socket, int resourceId)
             throws RemoteException {
         try {
             mSrvConfig
@@ -1221,13 +1209,7 @@
         pw.println("NetdNativeService Connection: " + (isNetdAlive() ? "alive" : "dead"));
         pw.println();
 
-        pw.println("mUserQuotaTracker:");
-        pw.println(mUserQuotaTracker);
-        pw.println("mTransformRecords:");
-        pw.println(mTransformRecords);
-        pw.println("mUdpSocketRecords:");
-        pw.println(mUdpSocketRecords);
-        pw.println("mSpiRecords:");
-        pw.println(mSpiRecords);
+        pw.println("mUserResourceTracker:");
+        pw.println(mUserResourceTracker);
     }
 }