• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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