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