1#!/usr/bin/env python 2# Copyright 2018 The Chromium Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""A helper script to list class verification errors. 7 8This is a wrapper around the device's oatdump executable, parsing desired output 9and accommodating API-level-specific details, such as file paths. 10""" 11 12from __future__ import print_function 13 14import argparse 15import exceptions 16import logging 17import os 18import re 19 20import devil_chromium 21from devil.android import device_errors 22from devil.android import device_temp_file 23from devil.android import device_utils 24from devil.android.ndk import abis 25from devil.android.sdk import version_codes 26from devil.android.tools import script_common 27from devil.utils import logging_common 28from py_utils import tempfile_ext 29 30STATUSES = [ 31 'NotReady', 32 'RetryVerificationAtRuntime', 33 'Verified', 34 'Initialized', 35 'SuperclassValidated', 36] 37 38 39def DetermineDeviceToUse(devices): 40 """Like DeviceUtils.HealthyDevices(), but only allow a single device. 41 42 Args: 43 devices: A (possibly empty) list of serial numbers, such as from the 44 --device flag. 45 Returns: 46 A single device_utils.DeviceUtils instance. 47 Raises: 48 device_errors.NoDevicesError: Raised when no non-denylisted devices exist. 49 device_errors.MultipleDevicesError: Raise when multiple devices exist, but 50 |devices| does not distinguish which to use. 51 """ 52 if not devices: 53 # If the user did not specify which device, we let HealthyDevices raise 54 # MultipleDevicesError. 55 devices = None 56 usable_devices = device_utils.DeviceUtils.HealthyDevices(device_arg=devices) 57 # If the user specified more than one device, we still only want to support a 58 # single device, so we explicitly raise MultipleDevicesError. 59 if len(usable_devices) > 1: 60 raise device_errors.MultipleDevicesError(usable_devices) 61 return usable_devices[0] 62 63 64class DeviceOSError(Exception): 65 """Raised when a file is missing from the device, or something similar.""" 66 pass 67 68 69class UnsupportedDeviceError(Exception): 70 """Raised when the device is not supported by this script.""" 71 pass 72 73 74def _GetFormattedArch(device): 75 abi = device.product_cpu_abi 76 # Some architectures don't map 1:1 with the folder names. 77 return {abis.ARM_64: 'arm64', abis.ARM: 'arm'}.get(abi, abi) 78 79 80def PathToDexForPlatformVersion(device, package_name): 81 """Gets the full path to the dex file on the device.""" 82 sdk_level = device.build_version_sdk 83 paths_to_apk = device.GetApplicationPaths(package_name) 84 if not paths_to_apk: 85 raise DeviceOSError( 86 'Could not find data directory for {}. Is it installed?'.format( 87 package_name)) 88 if len(paths_to_apk) != 1: 89 raise DeviceOSError( 90 'Expected exactly one path for {} but found {}'.format( 91 package_name, 92 paths_to_apk)) 93 path_to_apk = paths_to_apk[0] 94 95 if version_codes.LOLLIPOP <= sdk_level <= version_codes.LOLLIPOP_MR1: 96 # Of the form "com.example.foo-\d", where \d is some digit (usually 1 or 2) 97 package_with_suffix = os.path.basename(os.path.dirname(path_to_apk)) 98 arch = _GetFormattedArch(device) 99 dalvik_prefix = '/data/dalvik-cache/{arch}'.format(arch=arch) 100 odex_file = '{prefix}/data@app@{package}@base.apk@classes.dex'.format( 101 prefix=dalvik_prefix, 102 package=package_with_suffix) 103 elif sdk_level >= version_codes.MARSHMALLOW: 104 arch = _GetFormattedArch(device) 105 odex_file = '{data_dir}/oat/{arch}/base.odex'.format( 106 data_dir=os.path.dirname(path_to_apk), arch=arch) 107 else: 108 raise UnsupportedDeviceError('Unsupported API level: {}'.format(sdk_level)) 109 110 odex_file_exists = device.FileExists(odex_file) 111 if odex_file_exists: 112 return odex_file 113 elif sdk_level >= version_codes.PIE: 114 raise DeviceOSError( 115 'Unable to find odex file: you must run dex2oat on debuggable apps ' 116 'on >= P after installation.') 117 raise DeviceOSError('Unable to find odex file ' + odex_file) 118 119 120def _AdbOatDumpForPackage(device, package_name, out_file): 121 """Runs oatdump on the device.""" 122 # Get the path to the odex file. 123 odex_file = PathToDexForPlatformVersion(device, package_name) 124 device.RunShellCommand( 125 ['oatdump', '--oat-file=' + odex_file, '--output=' + out_file], 126 timeout=420, 127 shell=True, 128 check_return=True) 129 130 131class JavaClass(object): 132 """This represents a Java Class and its ART Class Verification status.""" 133 134 def __init__(self, name, verification_status): 135 self.name = name 136 self.verification_status = verification_status 137 138 139def _ParseMappingFile(proguard_map_file): 140 """Creates a map of obfuscated names to deobfuscated names.""" 141 mappings = {} 142 with open(proguard_map_file, 'r') as f: 143 pattern = re.compile(r'^(\S+) -> (\S+):') 144 for line in f: 145 m = pattern.match(line) 146 if m is not None: 147 deobfuscated_name = m.group(1) 148 obfuscated_name = m.group(2) 149 mappings[obfuscated_name] = deobfuscated_name 150 return mappings 151 152 153def _DeobfuscateJavaClassName(dex_code_name, proguard_mappings): 154 return proguard_mappings.get(dex_code_name, dex_code_name) 155 156 157def FormatJavaClassName(dex_code_name, proguard_mappings): 158 obfuscated_name = dex_code_name.replace('/', '.') 159 if proguard_mappings is not None: 160 return _DeobfuscateJavaClassName(obfuscated_name, proguard_mappings) 161 else: 162 return obfuscated_name 163 164 165def ListClassesAndVerificationStatus(oatdump_output, proguard_mappings): 166 """Lists all Java classes in the dex along with verification status.""" 167 java_classes = [] 168 pattern = re.compile(r'\d+: L([^;]+).*\(type_idx=[^(]+\((\w+)\).*') 169 for line in oatdump_output: 170 m = pattern.match(line) 171 if m is not None: 172 name = FormatJavaClassName(m.group(1), proguard_mappings) 173 # Some platform levels prefix this with "Status" while other levels do 174 # not. Strip this for consistency. 175 verification_status = m.group(2).replace('Status', '') 176 java_classes.append(JavaClass(name, verification_status)) 177 return java_classes 178 179 180def _PrintVerificationResults(target_status, java_classes, show_summary): 181 """Prints results for user output.""" 182 # Sort to keep output consistent between runs. 183 java_classes.sort(key=lambda c: c.name) 184 d = {} 185 for status in STATUSES: 186 d[status] = 0 187 188 for java_class in java_classes: 189 if java_class.verification_status == target_status: 190 print(java_class.name) 191 if java_class.verification_status not in d: 192 raise exceptions.RuntimeError('Unexpected status: {0}'.format( 193 java_class.verification_status)) 194 else: 195 d[java_class.verification_status] += 1 196 197 if show_summary: 198 for status in d: 199 count = d[status] 200 print('Total {status} classes: {num}'.format( 201 status=status, num=count)) 202 print('Total number of classes: {num}'.format( 203 num=len(java_classes))) 204 205 206def RealMain(mapping, device_arg, package, status, hide_summary, workdir): 207 if mapping is None: 208 logging.warn('Skipping deobfuscation because no map file was provided.') 209 device = DetermineDeviceToUse(device_arg) 210 device.EnableRoot() 211 with device_temp_file.DeviceTempFile( 212 device.adb) as file_on_device: 213 _AdbOatDumpForPackage(device, package, file_on_device.name) 214 file_on_host = os.path.join(workdir, 'out.dump') 215 device.PullFile(file_on_device.name, file_on_host, timeout=220) 216 proguard_mappings = (_ParseMappingFile(mapping) if mapping else None) 217 with open(file_on_host, 'r') as f: 218 java_classes = ListClassesAndVerificationStatus(f, proguard_mappings) 219 _PrintVerificationResults(status, java_classes, not hide_summary) 220 221 222def main(): 223 parser = argparse.ArgumentParser(description=""" 224List Java classes in an APK which fail ART class verification. 225""") 226 parser.add_argument( 227 '--package', 228 '-P', 229 type=str, 230 default=None, 231 required=True, 232 help='Specify the full application package name') 233 parser.add_argument( 234 '--mapping', 235 '-m', 236 type=os.path.realpath, 237 default=None, 238 help='Mapping file for the desired APK to deobfuscate class names') 239 parser.add_argument( 240 '--hide-summary', 241 default=False, 242 action='store_true', 243 help='Do not output the total number of classes in each Status.') 244 parser.add_argument( 245 '--status', 246 type=str, 247 default='RetryVerificationAtRuntime', 248 choices=STATUSES, 249 help='Which category of classes to list at the end of the script') 250 parser.add_argument( 251 '--workdir', 252 '-w', 253 type=os.path.realpath, 254 default=None, 255 help=('Work directory for oatdump output (default = temporary ' 256 'directory). If specified, this will not be cleaned up at the end ' 257 'of the script (useful if you want to inspect oatdump output ' 258 'manually)')) 259 260 script_common.AddEnvironmentArguments(parser) 261 script_common.AddDeviceArguments(parser) 262 logging_common.AddLoggingArguments(parser) 263 264 args = parser.parse_args() 265 devil_chromium.Initialize(adb_path=args.adb_path) 266 logging_common.InitializeLogging(args) 267 268 if args.workdir: 269 if not os.path.isdir(args.workdir): 270 raise RuntimeError('Specified working directory does not exist') 271 RealMain(args.mapping, args.devices, args.package, args.status, 272 args.hide_summary, args.workdir) 273 # Assume the user wants the workdir to persist (useful for debugging). 274 logging.warn('Not cleaning up explicitly-specified workdir: %s', 275 args.workdir) 276 else: 277 with tempfile_ext.NamedTemporaryDirectory() as workdir: 278 RealMain(args.mapping, args.devices, args.package, args.status, 279 args.hide_summary, workdir) 280 281 282if __name__ == '__main__': 283 main() 284