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.content.Context; 20 import android.content.pm.ApplicationInfo; 21 import android.content.pm.PackageManager; 22 import android.content.pm.PackageManager.NameNotFoundException; 23 import android.os.Handler; 24 import android.util.Log; 25 26 import com.android.internal.annotations.VisibleForTesting; 27 import com.android.server.sdksandbox.verifier.DexParser.DexEntry; 28 29 import java.io.File; 30 import java.io.IOException; 31 import java.util.ArrayList; 32 import java.util.List; 33 import java.util.Map; 34 import java.util.stream.Collectors; 35 36 /** 37 * Handles the loading of dex files for multiple apks to be verified, ensures that a single dex file 38 * is loaded at any time. 39 * 40 * @hide 41 */ 42 public class SerialDexLoader { 43 private static final String TAG = "SdkSandboxVerifier"; 44 45 private DexParser mParser; 46 private Handler mHandler; 47 private DexSymbols mDexSymbols; 48 SerialDexLoader(DexParser parser, Handler handler)49 public SerialDexLoader(DexParser parser, Handler handler) { 50 mParser = parser; 51 mHandler = handler; 52 mDexSymbols = new DexSymbols(); 53 } 54 55 /** 56 * Queues all dex files found for an apk for serially loading and analyzing. 57 * 58 * @param apkPathFile path to apk containing one or more dex files 59 * @param packagename packagename associated with the apk 60 * @param verificationHandler object to handle the verification of the loaded dex 61 */ queueApkToLoad( File apkPathFile, String packagename, Context context, VerificationHandler verificationHandler)62 public void queueApkToLoad( 63 File apkPathFile, 64 String packagename, 65 Context context, 66 VerificationHandler verificationHandler) { 67 68 mHandler.post( 69 () -> { 70 List<DexEntry> dexEntries = null; 71 try { 72 dexEntries = mParser.getDexFilePaths(apkPathFile); 73 } catch (IOException e) { 74 verificationHandler.onVerificationErrorForPackage(e); 75 return; 76 } 77 78 File installedPackageFile = null; 79 List<String> verifiedEntries = new ArrayList<>(); 80 boolean passedVerification = false; 81 try { 82 passedVerification = 83 verifyDexEntries(dexEntries, verifiedEntries, verificationHandler); 84 verificationHandler.onVerificationCompleteForPackage(passedVerification); 85 return; 86 } catch (IOException e) { 87 // tmp dex files were deleted while verifying, this happens when 88 // installation completes, try fetching installed apk to continue verifying 89 installedPackageFile = getInstalledPackageFile(packagename, context); 90 if (installedPackageFile == null) { 91 verificationHandler.onVerificationErrorForPackage( 92 new Exception("apk files not found for " + packagename)); 93 return; 94 } 95 } 96 97 // verify installed dex entries if we ran out of time to verify the tmp files 98 List<DexEntry> installedDexEntries = null; 99 try { 100 installedDexEntries = mParser.getDexFilePaths(installedPackageFile); 101 } catch (IOException e) { 102 verificationHandler.onVerificationErrorForPackage(e); 103 return; 104 } 105 106 // avoid verifying the same dex file twice 107 List<DexEntry> pendingDexEntries = 108 installedDexEntries.stream() 109 .filter( 110 entry -> 111 !verifiedEntries.contains( 112 entry.getEntryFilename())) 113 .collect(Collectors.toList()); 114 115 try { 116 passedVerification = 117 verifyDexEntries( 118 pendingDexEntries, verifiedEntries, verificationHandler); 119 verificationHandler.onVerificationCompleteForPackage(passedVerification); 120 return; 121 } catch (IOException e) { 122 verificationHandler.onVerificationErrorForPackage(e); 123 return; 124 } 125 }); 126 } 127 verifyDexEntries( List<DexEntry> dexEntries, List<String> verifiedEntries, VerificationHandler verificationHandler)128 private boolean verifyDexEntries( 129 List<DexEntry> dexEntries, 130 List<String> verifiedEntries, 131 VerificationHandler verificationHandler) 132 throws IOException { 133 for (DexEntry entry : dexEntries) { 134 mParser.loadDexSymbols(entry.getApkFile(), entry.getDexEntry(), mDexSymbols); 135 if (!verificationHandler.verify(mDexSymbols)) { 136 return false; 137 } 138 verifiedEntries.add(entry.getEntryFilename()); 139 } 140 return true; 141 } 142 getInstalledPackageFile(String packagename, Context context)143 private File getInstalledPackageFile(String packagename, Context context) { 144 try { 145 ApplicationInfo applicationInfo = 146 context.getPackageManager() 147 .getPackageInfo( 148 packagename, 149 PackageManager.MATCH_STATIC_SHARED_AND_SDK_LIBRARIES 150 | PackageManager.MATCH_ANY_USER) 151 .applicationInfo; 152 return new File(applicationInfo.sourceDir); 153 } catch (NameNotFoundException e) { 154 return null; 155 } 156 } 157 158 /** Interface for handling processing of the loaded dex contents */ 159 public interface VerificationHandler { 160 161 /** 162 * Takes in the DexSymbols and verifies its contents. 163 * 164 * @param result object contains the symbols parsed from the loaded dex file 165 */ verify(DexSymbols result)166 boolean verify(DexSymbols result); 167 168 /** 169 * Called when all the loaded dex files have passed verification, or when one has failed. 170 * 171 * @param passed is false if the last loaded dex failed verification, or true if all dexes 172 * passed. 173 */ onVerificationCompleteForPackage(boolean result)174 void onVerificationCompleteForPackage(boolean result); 175 176 /** 177 * Error occurred on verifying. 178 * 179 * @param e exception thrown while attempting to load and verify the apk. 180 */ onVerificationErrorForPackage(Exception e)181 void onVerificationErrorForPackage(Exception e); 182 } 183 184 /** Result class that contains symbols loaded from a DEX file */ 185 public static class DexSymbols { 186 187 private static final int DEX_MAX_METHOD_COUNT = 65536; 188 189 private String mDexEntry; 190 191 /** The table of classes referenced by the DEX file. */ 192 private ArrayList<String> mReferencedClasses = new ArrayList<>(DEX_MAX_METHOD_COUNT); 193 194 /** The table of methods referenced by the DEX file. */ 195 private ArrayList<String> mReferencedMethods = new ArrayList<>(DEX_MAX_METHOD_COUNT); 196 197 /** Maps referenced methods to their declaring class in the referenced classes table. */ 198 private ArrayList<Integer> mClassIndex = new ArrayList<>(DEX_MAX_METHOD_COUNT); 199 200 /** 201 * Adds a new method to the referencedMethods table and its containing class to the 202 * referenced classes table if it's different to the last seen class. 203 * 204 * <p>Referenced methods should be added in the order that they are present in the methods 205 * table from the dex file. 206 * 207 * @param classname describes the class with / as separator of its subpackages 208 * @param method the method name, parameter types and return types with ; as separator 209 */ addReferencedMethod(String classname, String method)210 public void addReferencedMethod(String classname, String method) { 211 // the method table is sorted by class, so new classnames can be stored when first 212 // encountered 213 if (mReferencedClasses.size() == 0 214 || !mReferencedClasses.get(mReferencedClasses.size() - 1).equals(classname)) { 215 mReferencedClasses.add(classname); 216 } 217 mReferencedMethods.add(method); 218 mClassIndex.add(mReferencedClasses.size() - 1); 219 } 220 221 @VisibleForTesting hasReferencedMethod(String classname, String method)222 boolean hasReferencedMethod(String classname, String method) { 223 int methodIdx = mReferencedMethods.indexOf(method); 224 return methodIdx >= 0 225 && mReferencedClasses.get(mClassIndex.get(methodIdx)).equals(classname); 226 } 227 228 /** 229 * Clears the internal state of DexSymbols and sets dex entry name to load next dex file. 230 */ clearAndSetDexEntry(String dexEntry)231 public void clearAndSetDexEntry(String dexEntry) { 232 this.mDexEntry = dexEntry; 233 mReferencedClasses.clear(); 234 mReferencedMethods.clear(); 235 mClassIndex.clear(); 236 } 237 238 /** Returns the number of referenced methods loaded for the current dex */ getReferencedMethodCount()239 public int getReferencedMethodCount() { 240 return mReferencedMethods.size(); 241 } 242 243 /** 244 * Returns the method indexed by methodIndex in the table of loaded methods from the dex 245 * file 246 */ getReferencedMethodAtIndex(int methodIndex)247 public String getReferencedMethodAtIndex(int methodIndex) { 248 if (methodIndex < 0 || methodIndex >= mReferencedMethods.size()) { 249 throw new IndexOutOfBoundsException("Method index out of bounds: " + methodIndex); 250 } 251 return mReferencedMethods.get(methodIndex); 252 } 253 254 /** Returns the declaring class for the method indexed by methodIndex */ getClassForMethodAtIndex(int methodIndex)255 public String getClassForMethodAtIndex(int methodIndex) { 256 if (methodIndex < 0 || methodIndex >= mReferencedMethods.size()) { 257 throw new IndexOutOfBoundsException("Method index out of bounds: " + methodIndex); 258 } 259 return mReferencedClasses.get(mClassIndex.get(methodIndex)); 260 } 261 262 @Override toString()263 public String toString() { 264 return "DexSymbols: " + mDexEntry; 265 } 266 } 267 } 268