[RTT2] limit max number of outstanding requests per UID

Prevent spamming of service. Limit number of oustanding requests to
a large enough number that no correct use of the API will reach.

Bug: 65015291
Test: unit tests and integration tests
Change-Id: I675227911994f145ecea6638f049444c34fb031f
diff --git a/service/java/com/android/server/wifi/rtt/RttServiceImpl.java b/service/java/com/android/server/wifi/rtt/RttServiceImpl.java
index e3e6dcf..ff722ae 100644
--- a/service/java/com/android/server/wifi/rtt/RttServiceImpl.java
+++ b/service/java/com/android/server/wifi/rtt/RttServiceImpl.java
@@ -42,8 +42,8 @@
 import android.os.UserHandle;
 import android.os.WorkSource;
 import android.util.Log;
+import android.util.SparseIntArray;
 
-import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.WakeupMessage;
 import com.android.server.wifi.Clock;
 import com.android.server.wifi.util.WifiPermissionsUtil;
@@ -77,14 +77,16 @@
 
     private RttServiceSynchronized mRttServiceSynchronized;
 
-    @VisibleForTesting
-    public static final String HAL_RANGING_TIMEOUT_TAG = TAG + " HAL Ranging Timeout";
+    /* package */ static final String HAL_RANGING_TIMEOUT_TAG = TAG + " HAL Ranging Timeout";
 
     private static final long HAL_RANGING_TIMEOUT_MS = 5_000; // 5 sec
 
     // TODO: b/69323456 convert to a settable value
     /* package */ static final long BACKGROUND_PROCESS_EXEC_GAP_MS = 1_800_000; // 30 min
 
+    // arbitrary, larger than anything reasonable
+    /* package */ static final int MAX_QUEUED_PER_UID = 20;
+
     public RttServiceImpl(Context context) {
         mContext = context;
     }
@@ -463,6 +465,19 @@
         private void queueRangingRequest(int uid, WorkSource workSource, IBinder binder,
                 IBinder.DeathRecipient dr, String callingPackage, RangingRequest request,
                 IRttCallback callback) {
+            if (isRequestorSpamming(workSource)) {
+                Log.w(TAG,
+                        "Work source " + workSource + " is spamming, dropping request: " + request);
+                binder.unlinkToDeath(dr, 0);
+                try {
+                    callback.onRangingFailure(RangingResultCallback.STATUS_CODE_FAIL);
+                } catch (RemoteException e) {
+                    Log.e(TAG,  "RttServiceSynchronized.queueRangingRequest: spamming, callback "
+                            + "failed -- " + e);
+                }
+                return;
+            }
+
             RttRequestInfo newRequest = new RttRequestInfo();
             newRequest.uid = uid;
             newRequest.workSource = workSource;
@@ -480,6 +495,30 @@
             executeNextRangingRequestIfPossible(false);
         }
 
+        private boolean isRequestorSpamming(WorkSource ws) {
+            if (VDBG) Log.v(TAG, "isRequestorSpamming: ws" + ws);
+
+            SparseIntArray counts = new SparseIntArray();
+
+            for (RttRequestInfo rri : mRttRequestQueue) {
+                for (int i = 0; i < rri.workSource.size(); ++i) {
+                    int uid = rri.workSource.get(i);
+                    counts.put(uid, counts.get(uid) + 1);
+                }
+            }
+
+            for (int i = 0; i < ws.size(); ++i) {
+                if (counts.get(ws.get(i)) < MAX_QUEUED_PER_UID) {
+                    return false;
+                }
+            }
+
+            if (VDBG) {
+                Log.v(TAG, "isRequestorSpamming: ws=" + ws + ", someone is spamming: " + counts);
+            }
+            return true;
+        }
+
         private void executeNextRangingRequestIfPossible(boolean popFirst) {
             if (VDBG) Log.v(TAG, "executeNextRangingRequestIfPossible: popFirst=" + popFirst);
 
diff --git a/tests/wifitests/src/com/android/server/wifi/rtt/RttServiceImplTest.java b/tests/wifitests/src/com/android/server/wifi/rtt/RttServiceImplTest.java
index 01e3750..ee8c15f 100644
--- a/tests/wifitests/src/com/android/server/wifi/rtt/RttServiceImplTest.java
+++ b/tests/wifitests/src/com/android/server/wifi/rtt/RttServiceImplTest.java
@@ -202,6 +202,9 @@
 
     @After
     public void tearDown() throws Exception {
+        assertEquals("Binder links != unlinks to death (size)",
+                mBinderLinkToDeathCounter.mUniqueExecs.size(),
+                mBinderUnlinkToDeathCounter.mUniqueExecs.size());
         assertEquals("Binder links != unlinks to death", mBinderLinkToDeathCounter.mUniqueExecs,
                 mBinderUnlinkToDeathCounter.mUniqueExecs);
     }
@@ -863,6 +866,125 @@
     }
 
     /**
+     * Validate that flooding the service with ranging requests will cause it to start rejecting
+     * rejects from the flooding uid. Single UID.
+     */
+    @Test
+    public void testRejectFloodingRequestsSingleUid() throws Exception {
+        runFloodRequestsTest(true);
+    }
+
+    /**
+     * Validate that flooding the service with ranging requests will cause it to start rejecting
+     * rejects from the flooding uid. WorkSource (all identical).
+     */
+    @Test
+    public void testRejectFloodingRequestsIdenticalWorksources() throws Exception {
+        runFloodRequestsTest(false);
+    }
+
+    /**
+     * Validate that flooding the service with ranging requests will cause it to start rejecting
+     * rejects from the flooding uid. WorkSource (with one constant UID but other varying UIDs -
+     * the varying UIDs should prevent the flood throttle)
+     */
+    @Test
+    public void testDontRejectFloodingRequestsVariousUids() throws Exception {
+        RangingRequest request = RttTestUtils.getDummyRangingRequest((byte) 1);
+        WorkSource ws = new WorkSource(10);
+
+        // 1. issue a request
+        mDut.startRanging(mockIbinder, mPackageName, ws, request, mockCallback);
+        mMockLooper.dispatchAll();
+
+        verify(mockNative).rangeRequest(mIntCaptor.capture(), eq(request));
+        verifyWakeupSet();
+
+        // 2. issue FLOOD LEVEL requests + 10 at various UIDs - no failure expected
+        for (int i = 0; i < RttServiceImpl.MAX_QUEUED_PER_UID + 10; ++i) {
+            WorkSource wsExtra = new WorkSource(ws);
+            wsExtra.add(11 + i);
+            mDut.startRanging(mockIbinder, mPackageName, wsExtra, request, mockCallback);
+        }
+        mMockLooper.dispatchAll();
+
+        // 3. clear queue
+        mDut.disable();
+        mMockLooper.dispatchAll();
+
+        verifyWakeupCancelled();
+        verify(mockNative).rangeCancel(eq(mIntCaptor.getValue()), any());
+        verify(mockCallback, times(RttServiceImpl.MAX_QUEUED_PER_UID + 11)).onRangingFailure(
+                RangingResultCallback.STATUS_CODE_FAIL_RTT_NOT_AVAILABLE);
+
+        verify(mockNative, atLeastOnce()).isReady();
+        verifyNoMoreInteractions(mockNative, mockCallback, mAlarmManager.getAlarmManager());
+    }
+
+    /**
+     * Utility to run configurable tests for flooding range requests.
+     * - Execute a single request
+     * - Flood service with requests: using same ID or same WorkSource
+     * - Provide results (to clear queue) and execute another test: validate succeeds
+     */
+    private void runFloodRequestsTest(boolean useUids) throws Exception {
+        RangingRequest request = RttTestUtils.getDummyRangingRequest((byte) 1);
+        Pair<List<RttResult>, List<RangingResult>> result = RttTestUtils.getDummyRangingResults(
+                request);
+
+        WorkSource ws = new WorkSource();
+        ws.add(10);
+        ws.add(20);
+        ws.add(30);
+
+        InOrder cbInorder = inOrder(mockCallback);
+        InOrder nativeInorder = inOrder(mockNative);
+
+        // 1. issue a request
+        mDut.startRanging(mockIbinder, mPackageName, useUids ? null : ws, request, mockCallback);
+        mMockLooper.dispatchAll();
+
+        nativeInorder.verify(mockNative).rangeRequest(mIntCaptor.capture(), eq(request));
+        verifyWakeupSet();
+
+        // 2. issue FLOOD LEVEL requests + 10: should get 11 failures (10 extra + 1 original)
+        for (int i = 0; i < RttServiceImpl.MAX_QUEUED_PER_UID + 10; ++i) {
+            mDut.startRanging(mockIbinder, mPackageName, useUids ? null : ws, request,
+                    mockCallback);
+        }
+        mMockLooper.dispatchAll();
+
+        cbInorder.verify(mockCallback, times(11)).onRangingFailure(
+                RangingResultCallback.STATUS_CODE_FAIL);
+
+        // 3. provide results
+        mDut.onRangingResults(mIntCaptor.getValue(), result.first);
+        mMockLooper.dispatchAll();
+
+        cbInorder.verify(mockCallback).onRangingResults(result.second);
+        verifyWakeupCancelled();
+
+        nativeInorder.verify(mockNative).rangeRequest(mIntCaptor.capture(), eq(request));
+        verifyWakeupSet();
+
+        // 4. issue a request: don't expect a failure
+        mDut.startRanging(mockIbinder, mPackageName, useUids ? null : ws, request, mockCallback);
+        mMockLooper.dispatchAll();
+
+        // 5. clear queue
+        mDut.disable();
+        mMockLooper.dispatchAll();
+
+        verifyWakeupCancelled();
+        nativeInorder.verify(mockNative).rangeCancel(eq(mIntCaptor.getValue()), any());
+        cbInorder.verify(mockCallback, times(RttServiceImpl.MAX_QUEUED_PER_UID)).onRangingFailure(
+                RangingResultCallback.STATUS_CODE_FAIL_RTT_NOT_AVAILABLE);
+
+        verify(mockNative, atLeastOnce()).isReady();
+        verifyNoMoreInteractions(mockNative, mockCallback, mAlarmManager.getAlarmManager());
+    }
+
+    /**
      * Validate that when Wi-Fi gets disabled (HAL level) the ranging queue gets cleared.
      */
     @Test