1# 2# Copyright (C) 2017 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. 15import json 16import logging 17import os 18import shutil 19import subprocess 20import tempfile 21import zipfile 22 23from vts.runners.host import keys 24from vts.utils.python.web import feature_utils 25from vts.utils.python.controllers.adb import AdbError 26from vts.utils.python.coverage import sancov_parser 27 28 29class SancovFeature(feature_utils.Feature): 30 """Feature object for sanitizer coverage functionality. 31 32 Attributes: 33 enabled: boolean, True if sancov is enabled, False otherwise 34 web: (optional) WebFeature, object storing web feature util for test run 35 """ 36 _DEFAULT_EXCLUDE_PATHS = [ 37 'bionic', 'external/libcxx', 'system/core', 'system/libhidl' 38 ] 39 _TOGGLE_PARAM = keys.ConfigKeys.IKEY_ENABLE_SANCOV 40 _REQUIRED_PARAMS = [keys.ConfigKeys.IKEY_ANDROID_DEVICE] 41 42 _PROCESS_INIT_COMMAND = ( 43 '\"echo coverage=1 > /data/asan/system/asan.options.{0} && ' 44 'echo coverage_dir={1}/{2} >> /data/asan/system/asan.options.{0} && ' 45 'rm -rf {1}/{2} &&' 46 'mkdir {1}/{2} && ' 47 'killall {0}\"') 48 _FLUSH_COMMAND = '/data/local/tmp/vts_coverage_configure flush {0}' 49 _TARGET_SANCOV_PATH = '/data/misc/trace' 50 _SEARCH_PATHS = [(os.path.join('data', 'asan', 'vendor', 'bin'), 51 None), (os.path.join('vendor', 'bin'), None), 52 (os.path.join('data', 'asan', 'vendor', 'lib'), 53 32), (os.path.join('vendor', 'lib'), 32), (os.path.join( 54 'data', 'asan', 'vendor', 55 'lib64'), 64), (os.path.join('vendor', 'lib64'), 64)] 56 57 _BUILD_INFO = 'BUILD_INFO' 58 _REPO_DICT = 'repo-dict' 59 _SYMBOLS_ZIP = 'symbols.zip' 60 61 def __init__(self, 62 user_params, 63 web=None, 64 exclude_paths=_DEFAULT_EXCLUDE_PATHS): 65 """Initializes the sanitizer coverage feature. 66 67 Args: 68 user_params: A dictionary from parameter name (String) to parameter value. 69 web: (optional) WebFeature, object storing web feature util for test run. 70 exclude_paths: (optional) list of strings, paths to exclude for coverage. 71 """ 72 self.ParseParameters( 73 self._TOGGLE_PARAM, self._REQUIRED_PARAMS, user_params=user_params) 74 self.web = web 75 self._device_resource_dict = {} 76 self._file_vectors = {} 77 self._exclude_paths = exclude_paths 78 if self.enabled: 79 android_devices = getattr(self, 80 keys.ConfigKeys.IKEY_ANDROID_DEVICE) 81 if not isinstance(android_devices, list): 82 logging.warn('Android device information not available') 83 self.enabled = False 84 for device in android_devices: 85 serial = str(device.get(keys.ConfigKeys.IKEY_SERIAL)) 86 sancov_resource_path = str( 87 device.get(keys.ConfigKeys.IKEY_SANCOV_RESOURCES_PATH)) 88 if not serial or not sancov_resource_path: 89 logging.warn('Missing sancov information in device: %s', 90 device) 91 continue 92 self._device_resource_dict[serial] = sancov_resource_path 93 if self.enabled: 94 logging.info('Sancov is enabled.') 95 else: 96 logging.debug('Sancov is disabled.') 97 98 def InitializeDeviceCoverage(self, dut, hals): 99 """Initializes the sanitizer coverage on the device for the provided HAL. 100 101 Args: 102 dut: The device under test. 103 hals: A list of the HAL name and version (string) for which to 104 measure coverage (e.g. ['android.hardware.light@2.0']) 105 """ 106 serial = dut.adb.shell('getprop ro.serialno').strip() 107 if serial not in self._device_resource_dict: 108 logging.error("Invalid device provided: %s", serial) 109 return 110 111 for hal in hals: 112 entries = dut.adb.shell( 113 'lshal -itp 2> /dev/null | grep {0}'.format(hal)).splitlines() 114 pids = set([ 115 pid.strip() 116 for pid in map(lambda entry: entry.split()[-1], entries) 117 if pid.isdigit() 118 ]) 119 120 if len(pids) == 0: 121 logging.warn('No matching processes IDs found for HAL %s', hal) 122 return 123 processes = dut.adb.shell('ps -p {0} -o comm='.format( 124 ' '.join(pids))).splitlines() 125 process_names = set([ 126 name.strip() for name in processes 127 if name.strip() and not name.endswith(' (deleted)') 128 ]) 129 130 if len(process_names) == 0: 131 logging.warn('No matching processes names found for HAL %s', 132 hal) 133 return 134 135 for process_name in process_names: 136 cmd = self._PROCESS_INIT_COMMAND.format( 137 process_name, self._TARGET_SANCOV_PATH, hal) 138 try: 139 dut.adb.shell(cmd.format(process_name)) 140 except AdbError as e: 141 logging.error('Command failed: \"%s\"', cmd) 142 continue 143 144 def FlushDeviceCoverage(self, dut, hals): 145 """Flushes the sanitizer coverage on the device for the provided HAL. 146 147 Args: 148 dut: The device under test. 149 hals: A list of HAL name and version (string) for which to flush 150 coverage (e.g. ['android.hardware.light@2.0-service']) 151 """ 152 serial = dut.adb.shell('getprop ro.serialno').strip() 153 if serial not in self._device_resource_dict: 154 logging.error('Invalid device provided: %s', serial) 155 return 156 for hal in hals: 157 dut.adb.shell(self._FLUSH_COMMAND.format(hal)) 158 159 def _InitializeFileVectors(self, serial, binary_path): 160 """Parse the binary and read the debugging information. 161 162 Parse the debugging information in the binary to determine executable lines 163 of code for all of the files included in the binary. 164 165 Args: 166 serial: The serial of the device under test. 167 binary_path: The path to the unstripped binary on the host. 168 """ 169 file_vectors = self._file_vectors[serial] 170 args = ['readelf', '--debug-dump=decodedline', binary_path] 171 with tempfile.TemporaryFile('w+b') as tmp: 172 subprocess.call(args, stdout=tmp) 173 tmp.seek(0) 174 file = None 175 for entry in tmp: 176 entry_parts = entry.split() 177 if len(entry_parts) == 0: 178 continue 179 elif len(entry_parts) < 3 and entry_parts[-1].endswith(':'): 180 file = entry_parts[-1].rsplit(':')[0] 181 for path in self._exclude_paths: 182 if file.startswith(path): 183 file = None 184 break 185 continue 186 elif len(entry_parts) == 3 and file is not None: 187 line_no_string = entry_parts[1] 188 try: 189 line = int(line_no_string) 190 except ValueError: 191 continue 192 if file not in file_vectors: 193 file_vectors[file] = [-1] * line 194 if line > len(file_vectors[file]): 195 file_vectors[file].extend( 196 [-2] * (line - len(file_vectors[file]))) 197 file_vectors[file][line - 1] = 0 198 199 def _UpdateLineCounts(self, serial, lines): 200 """Update the line counts with the symbolized output lines. 201 202 Increment the line counts using the symbolized line information. 203 204 Args: 205 serial: The serial of the device under test. 206 lines: A list of strings in the format returned by addr2line (e.g. <file>:<line no>). 207 """ 208 file_vectors = self._file_vectors[serial] 209 for line in lines: 210 file, line_no_string = line.rsplit(':', 1) 211 if file == '??': # some lines cannot be symbolized and will report as '??' 212 continue 213 try: 214 line_no = int(line_no_string) 215 except ValueError: 216 continue # some lines cannot be symbolized and will report as '??' 217 if not file in file_vectors: # file is excluded 218 continue 219 if line_no > len(file_vectors[file]): 220 file_vectors[file].extend([-1] * 221 (line_no - len(file_vectors[file]))) 222 if file_vectors[file][line_no - 1] < 0: 223 file_vectors[file][line_no - 1] = 0 224 file_vectors[file][line_no - 1] += 1 225 226 def Upload(self): 227 """Append the coverage information to the web proto report. 228 """ 229 if not self.web or not self.web.enabled: 230 return 231 232 for device_serial in self._device_resource_dict: 233 resource_path = self._device_resource_dict[device_serial] 234 rev_map = json.load( 235 open(os.path.join(resource_path, 236 self._BUILD_INFO)))[self._REPO_DICT] 237 238 for file in self._file_vectors[device_serial]: 239 240 # Get the git project information 241 # Assumes that the project name and path to the project root are similar 242 revision = None 243 for project_name in rev_map: 244 # Matches when source file root and project name are the same 245 if file.startswith(str(project_name)): 246 git_project_name = str(project_name) 247 git_project_path = str(project_name) 248 revision = str(rev_map[project_name]) 249 break 250 251 parts = os.path.normpath(str(project_name)).split( 252 os.sep, 1) 253 # Matches when project name has an additional prefix before the project path root. 254 if len(parts) > 1 and file.startswith(parts[-1]): 255 git_project_name = str(project_name) 256 git_project_path = parts[-1] 257 revision = str(rev_map[project_name]) 258 break 259 260 if not revision: 261 logging.info("Could not find git info for %s", file) 262 continue 263 264 covered_count = sum( 265 map(lambda count: 1 if count > 0 else 0, 266 self._file_vectors[device_serial][file])) 267 total_count = sum( 268 map(lambda count: 1 if count >= 0 else 0, 269 self._file_vectors[device_serial][file])) 270 self.web.AddCoverageReport( 271 self._file_vectors[device_serial][file], file, 272 git_project_name, git_project_path, revision, 273 covered_count, total_count, True) 274 275 def ProcessDeviceCoverage(self, dut, hals): 276 """Process device coverage. 277 278 Fetch sancov files from the target, parse the sancov files, symbolize the output, 279 and update the line counters. 280 281 Args: 282 dut: The device under test. 283 hals: A list of HAL name and version (string) for which to process 284 coverage (e.g. ['android.hardware.light@2.0']) 285 """ 286 serial = dut.adb.shell('getprop ro.serialno').strip() 287 product = dut.adb.shell('getprop ro.build.product').strip() 288 289 if not serial in self._device_resource_dict: 290 logging.error('Invalid device provided: %s', serial) 291 return 292 293 if serial not in self._file_vectors: 294 self._file_vectors[serial] = {} 295 296 symbols_zip = zipfile.ZipFile( 297 os.path.join(self._device_resource_dict[serial], 298 self._SYMBOLS_ZIP)) 299 300 sancov_files = [] 301 for hal in hals: 302 sancov_files.extend( 303 dut.adb.shell('find {0}/{1} -name \"*.sancov\"'.format( 304 self._TARGET_SANCOV_PATH, hal)).splitlines()) 305 temp_dir = tempfile.mkdtemp() 306 307 binary_to_sancov = {} 308 for file in sancov_files: 309 dut.adb.pull(file, temp_dir) 310 binary, pid, _ = os.path.basename(file).rsplit('.', 2) 311 bitness, offsets = sancov_parser.ParseSancovFile( 312 os.path.join(temp_dir, os.path.basename(file))) 313 binary_to_sancov[binary] = (bitness, offsets) 314 315 for hal in hals: 316 dut.adb.shell('rm -rf {0}/{1}'.format(self._TARGET_SANCOV_PATH, 317 hal)) 318 319 search_root = os.path.join('out', 'target', 'product', product, 320 'symbols') 321 for path, bitness in self._SEARCH_PATHS: 322 for name in [ 323 f for f in symbols_zip.namelist() 324 if f.startswith(os.path.join(search_root, path)) 325 ]: 326 basename = os.path.basename(name) 327 if basename in binary_to_sancov and ( 328 bitness is None 329 or binary_to_sancov[basename][0] == bitness): 330 with symbols_zip.open( 331 name) as source, tempfile.NamedTemporaryFile( 332 'w+b') as target: 333 shutil.copyfileobj(source, target) 334 target.seek(0) 335 self._InitializeFileVectors(serial, target.name) 336 addrs = map(lambda addr: '{0:#x}'.format(addr), 337 binary_to_sancov[basename][1]) 338 args = ['addr2line', '-pe', target.name] 339 args.extend(addrs) 340 with tempfile.TemporaryFile('w+b') as tmp: 341 subprocess.call(args, stdout=tmp) 342 tmp.seek(0) 343 c = tmp.read().split() 344 self._UpdateLineCounts(serial, c) 345 del binary_to_sancov[basename] 346 shutil.rmtree(temp_dir) 347