1#!/usr/bin/env vpython3 2# Copyright 2018 The Chromium Authors 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 12 13 14import argparse 15import dataclasses # pylint: disable=wrong-import-order 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 67 68class UnsupportedDeviceError(Exception): 69 """Raised when the device is not supported by this script.""" 70 71 72def _GetFormattedArch(device): 73 abi = device.product_cpu_abi 74 # Some architectures don't map 1:1 with the folder names. 75 return {abis.ARM_64: 'arm64', abis.ARM: 'arm'}.get(abi, abi) 76 77 78def FindOdexFiles(device, package_name): 79 """Gets the full paths to the dex files on the device.""" 80 sdk_level = device.build_version_sdk 81 paths_to_apk = device.GetApplicationPaths(package_name) 82 if not paths_to_apk: 83 raise DeviceOSError( 84 'Could not find data directory for {}. Is it installed?'.format( 85 package_name)) 86 87 ret = [] 88 for path_to_apk in paths_to_apk: 89 if version_codes.LOLLIPOP <= sdk_level <= version_codes.LOLLIPOP_MR1: 90 # Of the form "com.example.foo-\d", where \d is a digit (usually 1 or 2). 91 package_with_suffix = os.path.basename(os.path.dirname(path_to_apk)) 92 arch = _GetFormattedArch(device) 93 dalvik_prefix = '/data/dalvik-cache/{arch}'.format(arch=arch) 94 odex_file = '{prefix}/data@app@{package}@base.apk@classes.dex'.format( 95 prefix=dalvik_prefix, package=package_with_suffix) 96 elif sdk_level >= version_codes.MARSHMALLOW: 97 arch = _GetFormattedArch(device) 98 odex_file = '{data_dir}/oat/{arch}/base.odex'.format( 99 data_dir=os.path.dirname(path_to_apk), arch=arch) 100 else: 101 raise UnsupportedDeviceError( 102 'Unsupported API level: {}'.format(sdk_level)) 103 104 odex_file_exists = device.FileExists(odex_file) 105 if odex_file_exists: 106 ret.append(odex_file) 107 elif sdk_level >= version_codes.PIE: 108 raise DeviceOSError( 109 'Unable to find odex file: you must run dex2oat on debuggable apps ' 110 'on >= P after installation.') 111 else: 112 raise DeviceOSError('Unable to find odex file ' + odex_file) 113 return ret 114 115 116def _AdbOatDump(device, odex_file, out_file): 117 """Runs oatdump on the device.""" 118 # Get the path to the odex file. 119 with device_temp_file.DeviceTempFile(device.adb) as device_file: 120 device.RunShellCommand( 121 ['oatdump', '--oat-file=' + odex_file, '--output=' + device_file.name], 122 timeout=420, 123 shell=True, 124 check_return=True) 125 device.PullFile(device_file.name, out_file, timeout=220) 126 127 128@dataclasses.dataclass(order=True, frozen=True) 129class JavaClass: 130 """This represents a Java Class and its ART Class Verification status.""" 131 name: str 132 verification_status: str 133 134 135def _ParseMappingFile(proguard_map_file): 136 """Creates a map of obfuscated names to deobfuscated names.""" 137 mappings = {} 138 with open(proguard_map_file, 'r') as f: 139 pattern = re.compile(r'^(\S+) -> (\S+):') 140 for line in f: 141 m = pattern.match(line) 142 if m is not None: 143 deobfuscated_name = m.group(1) 144 obfuscated_name = m.group(2) 145 mappings[obfuscated_name] = deobfuscated_name 146 return mappings 147 148 149def _DeobfuscateJavaClassName(dex_code_name, proguard_mappings): 150 return proguard_mappings.get(dex_code_name, dex_code_name) 151 152 153def FormatJavaClassName(dex_code_name, proguard_mappings): 154 obfuscated_name = dex_code_name.replace('/', '.') 155 if proguard_mappings is not None: 156 return _DeobfuscateJavaClassName(obfuscated_name, proguard_mappings) 157 return obfuscated_name 158 159 160def ParseOatdump(oatdump_output, proguard_mappings): 161 """Lists all Java classes in the dex along with verification status.""" 162 java_classes = [] 163 pattern = re.compile(r'\d+: L([^;]+).*\(type_idx=[^(]+\((\w+)\).*') 164 for line in oatdump_output: 165 m = pattern.match(line) 166 if m is not None: 167 name = FormatJavaClassName(m.group(1), proguard_mappings) 168 # Some platform levels prefix this with "Status" while other levels do 169 # not. Strip this for consistency. 170 verification_status = m.group(2).replace('Status', '') 171 java_classes.append(JavaClass(name, verification_status)) 172 return java_classes 173 174 175def _PrintVerificationResults(target_status, java_classes, show_summary): 176 """Prints results for user output.""" 177 # Sort to keep output consistent between runs. 178 java_classes.sort(key=lambda c: c.name) 179 d = {} 180 for status in STATUSES: 181 d[status] = 0 182 183 for java_class in java_classes: 184 if java_class.verification_status == target_status: 185 print(java_class.name) 186 if java_class.verification_status not in d: 187 raise RuntimeError('Unexpected status: {0}'.format( 188 java_class.verification_status)) 189 d[java_class.verification_status] += 1 190 191 if show_summary: 192 for status in d: 193 count = d[status] 194 print('Total {status} classes: {num}'.format( 195 status=status, num=count)) 196 print('Total number of classes: {num}'.format( 197 num=len(java_classes))) 198 199 200def RealMain(mapping, device_arg, package, status, hide_summary, workdir): 201 if mapping is None: 202 logging.warning('Skipping deobfuscation because no map file was provided.') 203 proguard_mappings = None 204 else: 205 proguard_mappings = _ParseMappingFile(mapping) 206 device = DetermineDeviceToUse(device_arg) 207 host_tempfile = os.path.join(workdir, 'out.dump') 208 device.EnableRoot() 209 odex_files = FindOdexFiles(device, package) 210 java_classes = set() 211 for odex_file in odex_files: 212 _AdbOatDump(device, odex_file, host_tempfile) 213 with open(host_tempfile, 'r') as f: 214 java_classes.update(ParseOatdump(f, proguard_mappings)) 215 _PrintVerificationResults(status, sorted(java_classes), not hide_summary) 216 217 218def main(): 219 parser = argparse.ArgumentParser(description=""" 220List Java classes in an APK which fail ART class verification. 221""") 222 parser.add_argument( 223 '--package', 224 '-P', 225 type=str, 226 default=None, 227 required=True, 228 help='Specify the full application package name') 229 parser.add_argument( 230 '--mapping', 231 '-m', 232 type=os.path.realpath, 233 default=None, 234 help='Mapping file for the desired APK to deobfuscate class names') 235 parser.add_argument( 236 '--hide-summary', 237 default=False, 238 action='store_true', 239 help='Do not output the total number of classes in each Status.') 240 parser.add_argument( 241 '--status', 242 type=str, 243 default='RetryVerificationAtRuntime', 244 choices=STATUSES, 245 help='Which category of classes to list at the end of the script') 246 parser.add_argument( 247 '--workdir', 248 '-w', 249 type=os.path.realpath, 250 default=None, 251 help=('Work directory for oatdump output (default = temporary ' 252 'directory). If specified, this will not be cleaned up at the end ' 253 'of the script (useful if you want to inspect oatdump output ' 254 'manually)')) 255 256 script_common.AddEnvironmentArguments(parser) 257 script_common.AddDeviceArguments(parser) 258 logging_common.AddLoggingArguments(parser) 259 260 args = parser.parse_args() 261 devil_chromium.Initialize(adb_path=args.adb_path) 262 logging_common.InitializeLogging(args) 263 264 if args.workdir: 265 if not os.path.isdir(args.workdir): 266 raise RuntimeError('Specified working directory does not exist') 267 RealMain(args.mapping, args.devices, args.package, args.status, 268 args.hide_summary, args.workdir) 269 # Assume the user wants the workdir to persist (useful for debugging). 270 logging.warning('Not cleaning up explicitly-specified workdir: %s', 271 args.workdir) 272 else: 273 with tempfile_ext.NamedTemporaryDirectory() as workdir: 274 RealMain(args.mapping, args.devices, args.package, args.status, 275 args.hide_summary, workdir) 276 277 278if __name__ == '__main__': 279 main() 280