1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.rkpdapp.stress;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 import static com.google.common.truth.Truth.assertWithMessage;
21 import static com.google.common.truth.TruthJUnit.assume;
22 
23 import android.content.Context;
24 import android.hardware.security.keymint.IRemotelyProvisionedComponent;
25 import android.os.IBinder;
26 import android.os.Process;
27 import android.os.ServiceManager;
28 import android.os.SystemProperties;
29 
30 import androidx.test.core.app.ApplicationProvider;
31 
32 import com.android.rkpdapp.IGetKeyCallback;
33 import com.android.rkpdapp.RemotelyProvisionedKey;
34 import com.android.rkpdapp.database.ProvisionedKey;
35 import com.android.rkpdapp.database.ProvisionedKeyDao;
36 import com.android.rkpdapp.database.RkpdDatabase;
37 import com.android.rkpdapp.interfaces.ServerInterface;
38 import com.android.rkpdapp.interfaces.ServiceManagerInterface;
39 import com.android.rkpdapp.interfaces.SystemInterface;
40 import com.android.rkpdapp.provisioner.Provisioner;
41 import com.android.rkpdapp.service.RegistrationBinder;
42 
43 import org.junit.Before;
44 import org.junit.Rule;
45 import org.junit.Test;
46 import org.junit.rules.TestName;
47 
48 import java.time.Duration;
49 import java.time.Instant;
50 import java.util.ArrayList;
51 import java.util.List;
52 import java.util.concurrent.CompletableFuture;
53 import java.util.concurrent.ExecutorService;
54 import java.util.concurrent.Executors;
55 
56 public class RegistrationBinderStressTest {
57     private static final int NUM_THREADS = Math.min(16, Runtime.getRuntime().availableProcessors());
58     private static final Duration STRESS_THREAD_TIME_LIMIT = Duration.ofSeconds(60);
59     private static final String SERVICE = IRemotelyProvisionedComponent.DESCRIPTOR + "/default";
60     private final ExecutorService mExecutor = Executors.newCachedThreadPool();
61     private Context mContext;
62     private SystemInterface mIrpcHal;
63     private ProvisionedKeyDao mKeyDao;
64 
65     @Rule
66     public final TestName mName = new TestName();
67 
68     @Before
setUp()69     public void setUp() throws Exception {
70         assume()
71                 .withMessage("The RKP server hostname is not configured -- assume RKP disabled.")
72                 .that(SystemProperties.get("remote_provisioning.hostname"))
73                 .isNotEmpty();
74         assume()
75                 .withMessage("Remotely Provisioned Component is not found -- RKP disabled.")
76                 .that(ServiceManager.isDeclared(SERVICE))
77                 .isTrue();
78         mContext = ApplicationProvider.getApplicationContext();
79         mIrpcHal = ServiceManagerInterface.getInstance(SERVICE);
80         mKeyDao = RkpdDatabase.getDatabase(mContext).provisionedKeyDao();
81         mKeyDao.deleteAllKeys();
82     }
83 
createRegistrationBinder()84     private RegistrationBinder createRegistrationBinder() {
85         boolean isAsync = false;
86         return new RegistrationBinder(mContext, Process.myUid(), mIrpcHal, mKeyDao,
87                 new ServerInterface(mContext, isAsync), new Provisioner(mContext, mKeyDao, isAsync),
88                 mExecutor);
89     }
90 
getKeyHelper(int keyId)91     private void getKeyHelper(int keyId) {
92         RegistrationBinder binder = createRegistrationBinder();
93         CompletableFuture<String> result = new CompletableFuture<>();
94         binder.getKey(keyId, new IGetKeyCallback.Stub() {
95             @Override
96             public void onSuccess(RemotelyProvisionedKey key) {
97                 result.complete("");
98             }
99 
100             @Override
101             public void onProvisioningNeeded() { /* noop */ }
102 
103             @Override
104             public void onCancel() {
105                 result.complete("Received unexpected cancel");
106             }
107 
108             @Override
109             public void onError(byte error, String description) {
110                 result.complete(description);
111             }
112 
113             @Override
114             public IBinder asBinder() {
115                 return this;
116             }
117         });
118         try {
119             assertThat(result.get()).isEmpty();
120         } catch (Exception e) {
121             assertWithMessage("Unexpected exception: " + e).fail();
122         }
123     }
124 
125     @Test
testGetSameKeyInParallel()126     public void testGetSameKeyInParallel() throws Exception {
127         // Run through various operations on binder objects in parallel, ensuring operations
128         // work concurrently.
129         int keyId = 0;
130         List<Thread> stressThreads = new ArrayList<>();
131         Instant endOfTest = Instant.now().plus(STRESS_THREAD_TIME_LIMIT);
132         for (int i = 0; i < NUM_THREADS; ++i) {
133             System.out.println("Spinning up test thread " + i);
134             Thread t = new Thread(() -> {
135                 int counter = 0;
136                 while (Instant.now().isBefore(endOfTest)) {
137                     ++counter;
138                     if (counter % 1000 == 0) {
139                         System.out.println("Thread " + Thread.currentThread().getId()
140                                 + " on iteration " + counter);
141                     }
142                     getKeyHelper(keyId);
143 
144                     // Clear the key assignment so that it can be re-used without hitting the RKP
145                     // server again. It's possible another thread already unassigned the key, so
146                     // we cannot assume we get a key here.
147                     ProvisionedKey key = mKeyDao.getKeyForClientAndIrpc(mIrpcHal.getServiceName(),
148                             Process.myUid(), keyId);
149                     if (key != null) {
150                         key.keyId = null;
151                         key.clientUid = null;
152                         mKeyDao.updateKey(key);
153                     }
154                 }
155             });
156             t.start();
157             stressThreads.add(t);
158         }
159 
160         for (Thread t : stressThreads) {
161             t.join();
162         }
163     }
164 }
165