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