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