• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.server.sdksandbox.verifier;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.content.Context;
22 import android.os.Handler;
23 import android.os.HandlerThread;
24 import android.os.OutcomeReceiver;
25 import android.os.Process;
26 import android.os.SystemClock;
27 import android.util.Log;
28 
29 import com.android.internal.annotations.GuardedBy;
30 import com.android.internal.annotations.VisibleForTesting;
31 import com.android.server.sdksandbox.proto.Verifier.AllowedApi;
32 import com.android.server.sdksandbox.proto.Verifier.AllowedApisList;
33 import com.android.server.sdksandbox.verifier.SerialDexLoader.DexSymbols;
34 import com.android.server.sdksandbox.verifier.SerialDexLoader.VerificationHandler;
35 
36 import com.google.protobuf.InvalidProtocolBufferException;
37 
38 import java.io.File;
39 import java.io.FileNotFoundException;
40 import java.io.IOException;
41 import java.util.ArrayList;
42 import java.util.Arrays;
43 import java.util.Collections;
44 import java.util.HashMap;
45 import java.util.List;
46 import java.util.Map;
47 
48 /**
49  * Verifies the SDK being installed against the APIs allowlist. Verification runs on DEX files of
50  * the SDK package.
51  *
52  * @hide
53  */
54 public class SdkDexVerifier {
55 
56     private final Object mPlatformApiAllowlistsLock = new Object();
57     private final Object mVerificationLock = new Object();
58 
59     private static final String TAG = "SdkSandboxVerifier";
60 
61     private static final String WILDCARD = "*";
62     private static final String EMPTY_STRING = "";
63 
64     private static final AllowedApi[] DEFAULT_RULES = {
65         AllowedApi.newBuilder().setClassName("Landroid/*").setAllow(false).build(),
66         AllowedApi.newBuilder().setClassName("Ljava/*").setAllow(false).build(),
67         AllowedApi.newBuilder().setClassName("Lcom/google/android/*").setAllow(false).build(),
68         AllowedApi.newBuilder().setClassName("Lcom/android/*").setAllow(false).build(),
69         AllowedApi.newBuilder().setClassName("Landroidx/*").setAllow(false).build(),
70     };
71 
72     private static SdkDexVerifier sSdkDexVerifier;
73     private ApiAllowlistProvider mApiAllowlistProvider;
74     private SerialDexLoader mDexLoader;
75 
76     // Maps targetSdkVersion to its allowlist
77     @GuardedBy("mPlatformApiAllowlistsLock")
78     private Map<Long, AllowedApisList> mPlatformApiAllowlists;
79 
80     @GuardedBy("mPlatformApiAllowlistsLock")
81     private Map<Long, StringTrie> mPlatformApiAllowTries = new HashMap<>();
82 
83     @GuardedBy("mVerificationLock")
84     private Map<String, Long> mVerificationTimes = new HashMap<>();
85 
86     /** Returns a singleton instance of {@link SdkDexVerifier} */
87     @NonNull
getInstance()88     public static SdkDexVerifier getInstance() {
89         synchronized (SdkDexVerifier.class) {
90             if (sSdkDexVerifier == null) {
91                 sSdkDexVerifier = new SdkDexVerifier(new Injector());
92             }
93         }
94         return sSdkDexVerifier;
95     }
96 
97     @VisibleForTesting
SdkDexVerifier(Injector injector)98     SdkDexVerifier(Injector injector) {
99         mApiAllowlistProvider = injector.getApiAllowlistProvider();
100         mDexLoader = injector.getDexLoader();
101     }
102 
103     /**
104      * Starts verification of the requested sdk
105      *
106      * @param sdkPath path to the sdk package to be verified
107      * @param targetSdkVersion Android SDK target version of the package being verified, declared in
108      *     the package manifest.
109      * @param callback to client for communication of parsing/verification results.
110      */
startDexVerification( String sdkPath, String packagename, long targetSdkVersion, Context context, OutcomeReceiver<VerificationResult, Exception> callback)111     public void startDexVerification(
112             String sdkPath,
113             String packagename,
114             long targetSdkVersion,
115             Context context,
116             OutcomeReceiver<VerificationResult, Exception> callback) {
117         long startTime = SystemClock.elapsedRealtime();
118         synchronized (mVerificationLock) {
119             mVerificationTimes.put(sdkPath, startTime);
120         }
121 
122         try {
123             initAllowlist(targetSdkVersion);
124         } catch (Exception e) {
125             callback.onError(e);
126             return;
127         }
128 
129         File sdkFile = new File(sdkPath);
130 
131         if (!sdkFile.exists()) {
132             callback.onError(new FileNotFoundException("Apk to verify not found: " + sdkPath));
133             return;
134         }
135 
136         mDexLoader.queueApkToLoad(
137                 sdkFile,
138                 packagename,
139                 context,
140                 new VerificationHandler() {
141                     private VerificationResult mLastDexResult;
142 
143                     @Override
144                     public boolean verify(DexSymbols dexSymbols) {
145 
146                         StringTrie<AllowedApi> verificationTrie;
147 
148                         synchronized (mPlatformApiAllowlistsLock) {
149                             verificationTrie = mPlatformApiAllowTries.get(targetSdkVersion);
150                         }
151 
152                         try {
153                             VerificationResult verificationResult =
154                                     verifyDexSymbols(dexSymbols, verificationTrie);
155                             Log.d(
156                                     TAG,
157                                     "Verification result for "
158                                             + dexSymbols.toString()
159                                             + ": "
160                                             + verificationResult.hasPassed());
161                             mLastDexResult = verificationResult;
162                             return verificationResult.hasPassed();
163                         } catch (Exception e) {
164                             callback.onError(e);
165                             return false;
166                         }
167                     }
168 
169                     @Override
170                     public void onVerificationCompleteForPackage(boolean passed) {
171                         if (passed) {
172                             Log.d(TAG, packagename + " verified.");
173                         } else {
174                             Log.d(TAG, packagename + " rejected");
175                         }
176                         logVerificationTime(packagename, sdkPath);
177                         callback.onResult(mLastDexResult);
178 
179                         // TODO(b/231441674): cache and log verification result
180                     }
181 
182                     @Override
183                     public void onVerificationErrorForPackage(Exception e) {
184                         logVerificationTime(packagename, sdkPath);
185                         callback.onError(e);
186                     }
187                 });
188     }
189 
logVerificationTime(String packagename, String sdkPath)190     private void logVerificationTime(String packagename, String sdkPath) {
191         synchronized (mVerificationLock) {
192             if (!mVerificationTimes.containsKey(sdkPath)) {
193                 return;
194             }
195             long verificationTime =
196                     SystemClock.elapsedRealtime() - mVerificationTimes.remove(sdkPath);
197             Log.d(
198                     TAG,
199                     "Verification time (ms) for package " + packagename + ": " + verificationTime);
200         }
201     }
202 
verifyDexSymbols( DexSymbols dexSymbols, StringTrie<AllowedApi> verificationTrie)203     VerificationResult verifyDexSymbols(
204             DexSymbols dexSymbols, StringTrie<AllowedApi> verificationTrie) {
205         // Initial capacity for instructions that can reference up to 256 registers for arguments.
206         // Most methods will have rather < 16 params plus a few tokens for the full class name.
207         ArrayList<String> tokens = new ArrayList<>(256);
208         ArrayList<String> restrictedUsages = new ArrayList<>();
209         for (int i = 0; i < dexSymbols.getReferencedMethodCount(); i++) {
210             tokens.clear();
211             tokens.addAll(Arrays.asList(dexSymbols.getClassForMethodAtIndex(i).split("/")));
212             tokens.addAll(Arrays.asList(dexSymbols.getReferencedMethodAtIndex(i).split(";")));
213 
214             AllowedApi rule = verificationTrie.retrieve(tokens.toArray(new String[tokens.size()]));
215             if (rule != null && !rule.getAllow()) {
216                 restrictedUsages.add(
217                         dexSymbols.getClassForMethodAtIndex(i)
218                                 + "->"
219                                 + dexSymbols.getReferencedMethodAtIndex(i));
220             }
221             // methods that don't match any rule are considered to be symbols defined in the
222             // package itself.
223         }
224         return new VerificationResult(restrictedUsages);
225     }
226 
227     /*
228      * Converts an AllowedApi object into an array of keys that will be added to the verification
229      * trie. The AllowedApi rules should follow TypeDescriptors semantics from the DEX format.
230      *
231      * The list of tokens is generated splitting the class name at '/' and adding the
232      * subsequent fields to be matched for equality or wildcard. The parameters list specifies
233      * all of the parameter types, its order is preserved from the method signature in
234      * the source file and there is no distinction between input and return parameters.
235      * Therefore, the order of the parameters in the rules matters when computing a rule match.
236      * Omitted fields will add a wildcard to match all possibilities.
237      *
238      * A fully qualified rule will match an exact method, an example:
239      * {
240      *      allow : false
241      *      class_name : "Landroid/view/inputmethod/InputMethodManager"
242      *      method_name : "getCurrentInputMethodSubtype"
243      *      parameters: ["V"]
244      *      return_type: "Landroid/view/inputmethod/InputMethodSubtype"
245      * }
246      * This rule will produce the list of tokens:
247      * ["Landroid", "view", "inputmethod", "InputMethodManager", "getCurrentInputMethodSubtype",
248      *      "V", "Landroid/view/inputmethod/InputMethodSubtype"]
249      *
250      * A generalized rule, that matches all methods in the InputMethodManager class that
251      * return an InputMethodSubtype object, will look like this:
252      * {
253      *      allow : false
254      *      class_name : "Landroid/view/inputmethod/InputMethodManager"
255      *      return_type: "Landroid/view/inputmethod/InputMethodSubtype"
256      * }
257      * This rule produces the list of tokens:
258      * ["Landroid", "view", "inputmethod", "InputMethodManager",
259      *      null, "Landroid/view/inputmethod/InputMethodSubtype"]
260      *
261      * Wildcards can be included in the class name to generalize to packages, for example:
262      * "Landroid/view/inputmethod/*" matches all classes within the android.view.inputmethod.
263      */
264     @VisibleForTesting
265     @Nullable
getApiTokens(AllowedApi apiRule)266     String[] getApiTokens(AllowedApi apiRule) {
267         ArrayList<String> tokens = new ArrayList<>();
268 
269         if (apiRule.getClassName().equals(EMPTY_STRING)) {
270             // match unspecified class name
271             tokens.add(WILDCARD);
272         } else {
273             List<String> classTokens = Arrays.asList(apiRule.getClassName().toString().split("/"));
274             tokens.addAll(classTokens);
275         }
276 
277         if (!apiRule.getMethodName().equals(EMPTY_STRING)) {
278             tokens.add(apiRule.getMethodName());
279         } else if (!WILDCARD.equals(tokens.get(tokens.size() - 1))) {
280             // match unspecified method name
281             tokens.add(WILDCARD);
282         }
283 
284         if (apiRule.getParametersCount() != 0) {
285             tokens.addAll(apiRule.getParametersList());
286         } else if (!WILDCARD.equals(tokens.get(tokens.size() - 1))) {
287             // match unspecified params
288             tokens.add(WILDCARD);
289         }
290 
291         if (!apiRule.getReturnType().equals(EMPTY_STRING)) {
292             tokens.add(apiRule.getReturnType());
293         } else if (!WILDCARD.equals(tokens.get(tokens.size() - 1))) {
294             // match unspecified return type
295             tokens.add(WILDCARD);
296         }
297 
298         // To catch a malformed rule like "Landroid//com"
299         if (tokens.contains(EMPTY_STRING)) {
300             return null;
301         }
302 
303         tokens.replaceAll(token -> token.equals(WILDCARD) ? null : token);
304 
305         return tokens.toArray(new String[tokens.size()]);
306     }
307 
308     /**
309      * Initializes the allowlist for a given target sandbox sdk version
310      *
311      * @param targetSdkVersion declared in the manifest of the installed package, different from
312      *     effectiveTargetSdkVersion.
313      */
initAllowlist(long targetSdkVersion)314     private void initAllowlist(long targetSdkVersion)
315             throws FileNotFoundException, InvalidProtocolBufferException, IOException {
316         synchronized (mPlatformApiAllowlistsLock) {
317             if (mPlatformApiAllowlists == null) {
318                 mPlatformApiAllowlists = mApiAllowlistProvider.loadPlatformApiAllowlist();
319             }
320 
321             if (!mPlatformApiAllowTries.containsKey(targetSdkVersion)) {
322                 buildAllowTrie(targetSdkVersion, mPlatformApiAllowlists.get(targetSdkVersion));
323             }
324         }
325     }
326 
327     @GuardedBy("mPlatformApiAllowlistsLock")
buildAllowTrie(long targetSdkVersion, AllowedApisList allowedApisList)328     private void buildAllowTrie(long targetSdkVersion, AllowedApisList allowedApisList) {
329         if (allowedApisList == null) {
330             Log.w(TAG, "No allowlist found for targetSdk " + targetSdkVersion);
331             return;
332         }
333 
334         StringTrie<AllowedApi> allowTrie = getBaseRuleTrie();
335 
336         for (AllowedApi apiRule : allowedApisList.getAllowedApisList()) {
337             String[] apiTokens = getApiTokens(apiRule);
338             if (apiTokens != null) {
339                 AllowedApi oldRule = allowTrie.put(apiRule, apiTokens);
340                 if (oldRule != null && oldRule.getAllow() != apiRule.getAllow()) {
341                     Log.w(
342                             TAG,
343                             "Rule was replaced for class "
344                                     + oldRule.getClassName()
345                                     + ". New rule value is: "
346                                     + apiRule.getAllow());
347                 }
348             } else {
349                 Log.w(TAG, "API Rule was malformed for rule with class " + apiRule.getClassName());
350                 return;
351             }
352         }
353 
354         mPlatformApiAllowTries.put(targetSdkVersion, allowTrie);
355     }
356 
getBaseRuleTrie()357     private StringTrie<AllowedApi> getBaseRuleTrie() {
358         StringTrie<AllowedApi> allowTrie = new StringTrie();
359         for (int i = 0; i < DEFAULT_RULES.length; i++) {
360             allowTrie.put(DEFAULT_RULES[i], getApiTokens(DEFAULT_RULES[i]));
361         }
362         return allowTrie;
363     }
364 
365     public static class VerificationResult {
366         private boolean mPassed;
367         private List<String> mRestrictedUsages;
368 
VerificationResult(List<String> restrictedUsages)369         public VerificationResult(List<String> restrictedUsages) {
370             mRestrictedUsages =
371                     restrictedUsages == null ? Collections.emptyList() : restrictedUsages;
372             mPassed = (mRestrictedUsages.size() == 0);
373         }
374 
375         /** Returns true if the restriction verification passes, false otherwise */
hasPassed()376         public boolean hasPassed() {
377             return mPassed;
378         }
379 
380         @VisibleForTesting
getRestrictedUsages()381         List<String> getRestrictedUsages() {
382             return mRestrictedUsages;
383         }
384     }
385 
386     static class Injector {
387         private ApiAllowlistProvider mAllowlistProvider;
388         private SerialDexLoader mDexLoader;
389 
Injector()390         Injector() {
391             mAllowlistProvider = new ApiAllowlistProvider();
392             HandlerThread handlerThread =
393                     new HandlerThread("DexParsingThread", Process.THREAD_PRIORITY_BACKGROUND);
394             handlerThread.start();
395             DexParser dexParser = new DexParserImpl();
396             mDexLoader = new SerialDexLoader(dexParser, new Handler(handlerThread.getLooper()));
397         }
398 
Injector(ApiAllowlistProvider apiAllowlistProvider, SerialDexLoader serialDexLoader)399         Injector(ApiAllowlistProvider apiAllowlistProvider, SerialDexLoader serialDexLoader) {
400             mAllowlistProvider = apiAllowlistProvider;
401             mDexLoader = serialDexLoader;
402         }
403 
getApiAllowlistProvider()404         ApiAllowlistProvider getApiAllowlistProvider() {
405             return mAllowlistProvider;
406         }
407 
getDexLoader()408         SerialDexLoader getDexLoader() {
409             return mDexLoader;
410         }
411     }
412 }
413