1# Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import fnmatch
6import glob
7import logging
8import os
9
10from autotest_lib.client.bin import test, utils
11from autotest_lib.client.common_lib import error
12from optparse import OptionParser
13
14FILE_CMD="file -m /usr/local/share/misc/magic.mgc"
15
16class ToolchainOptionSet:
17    """
18    Handles a set of hits, along with potential whitelists to ignore.
19    """
20    def __init__(self, description, bad_files, whitelist_file):
21        self.description = description
22        self.bad_set = set(bad_files.splitlines())
23        self.whitelist_set = set([])
24        self.process_whitelist_with_private(whitelist_file)
25
26
27    def process_whitelist_with_private(self, whitelist_file):
28        """
29        Filter out hits found on non-comment lines in the whitelist and
30        and private whitelist.
31
32        @param whitelist_file: path to whitelist file
33        """
34        whitelist_files = [whitelist_file]
35        private_file = os.path.join(os.path.dirname(whitelist_file),
36                                    "private_" +
37                                    os.path.basename(whitelist_file))
38        whitelist_files.append(private_file)
39        self.process_whitelists(whitelist_files)
40
41
42    def process_whitelist(self, whitelist_file):
43        """
44        Filter out hits found on non-comment lines in the whitelist.
45
46        @param whitelist_file: path to whitelist file
47        """
48        if os.path.isfile(whitelist_file):
49            f = open(whitelist_file)
50            whitelist = [x for x in f.read().splitlines()
51                                    if not x.startswith('#')]
52            f.close()
53            self.whitelist_set = self.whitelist_set.union(set(whitelist))
54
55        filtered_list = []
56        for bad_file in self.bad_set:
57            # Does |bad_file| match any entry in the whitelist?
58            in_whitelist = any([fnmatch.fnmatch(bad_file, whitelist_entry)
59                                for whitelist_entry in self.whitelist_set])
60            if not in_whitelist:
61                filtered_list.append(bad_file)
62
63        self.filtered_set = set(filtered_list)
64        # TODO(jorgelo): remove glob patterns from |new_passes|.
65        self.new_passes = self.whitelist_set.difference(self.bad_set)
66
67
68    def process_whitelists(self, whitelist_files):
69        """
70        Filter out hits found in a list of whitelist files.
71
72        @param whitelist_files: list of paths to whitelist files
73        """
74        for whitelist_file in whitelist_files:
75            self.process_whitelist(whitelist_file)
76
77
78    def get_fail_summary_message(self):
79        m = "Test %s " % self.description
80        m += "%d failures" % len(self.filtered_set)
81        return m
82
83
84    def get_fail_message(self):
85        m = self.get_fail_summary_message()
86        sorted_list = list(self.filtered_set)
87        sorted_list.sort()
88        m += "\nFAILED:\n%s\n\n" % "\n".join(sorted_list)
89        return m
90
91
92    def __str__(self):
93        m = "Test %s " % self.description
94        m += ("%d failures, %d in whitelist, %d in filtered, %d new passes " %
95              (len(self.bad_set),
96               len(self.whitelist_set),
97               len(self.filtered_set),
98               len(self.new_passes)))
99
100        if len(self.filtered_set):
101            sorted_list = list(self.filtered_set)
102            sorted_list.sort()
103            m += "FAILED:\n%s" % "\n".join(sorted_list)
104        else:
105            m += "PASSED!"
106
107        if len(self.new_passes):
108            sorted_list = list(self.new_passes)
109            sorted_list.sort()
110            m += ("\nNew passes (remove these from the whitelist):\n%s" %
111                  "\n".join(sorted_list))
112        logging.debug(m)
113        return m
114
115
116class platform_ToolchainOptions(test.test):
117    """
118    Tests for various expected conditions on ELF binaries in the image.
119    """
120    version = 2
121
122    def get_cmd(self, test_cmd, find_options=""):
123        base_cmd = ("find '%s' -wholename %s -prune -o "
124                    " -wholename /proc -prune -o "
125                    " -wholename /dev -prune -o "
126                    " -wholename /sys -prune -o "
127                    " -wholename /mnt/stateful_partition -prune -o "
128                    " -wholename /usr/local -prune -o "
129                    # There are files in /home/chronos that cause false
130                    # positives, and since that's noexec anyways, it should
131                    # be skipped.
132                    " -wholename '/home/chronos' -prune -o "
133                    " %s "
134                    " -not -name 'libstdc++.so.*' "
135                    " -not -name 'libgcc_s.so.*' "
136                    " -type f -executable -exec "
137                    "sh -c '%s "
138                    "{} | grep -q ELF && "
139                    "(%s || echo {})' ';'")
140        rootdir = "/"
141        cmd = base_cmd % (rootdir, self.autodir, find_options, FILE_CMD,
142                          test_cmd)
143        return cmd
144
145
146    def create_and_filter(self, description, cmd, whitelist_file,
147                          find_options=""):
148        """
149        Runs a command, with "{}" replaced (via "find -exec") with the
150        target ELF binary. If the command fails, the file is marked as
151        failing the test. Results are filtered against the provided
152        whitelist file.
153
154        @param description: text name of the check being done
155        @param cmd: command to run via find's -exec option
156        @param whitelist_file: list of failures to ignore
157        @param find_options: additional options for find to limit the scope
158        """
159        full_cmd = self.get_cmd(cmd, find_options)
160        bad_files = utils.system_output(full_cmd)
161        cso = ToolchainOptionSet(description, bad_files, whitelist_file)
162        cso.process_whitelist_with_private(whitelist_file)
163        return cso
164
165
166    def run_once(self, rootdir="/", args=[]):
167        """
168        Do a find for all the ELF files on the system.
169        For each one, test for compiler options that should have been used
170        when compiling the file.
171
172        For missing compiler options, print the files.
173        """
174
175        parser = OptionParser()
176        parser.add_option('--hardfp',
177                          dest='enable_hardfp',
178                          default=False,
179                          action='store_true',
180                          help='Whether to check for hardfp binaries.')
181        (options, args) = parser.parse_args(args)
182
183        option_sets = []
184
185        libc_glob = "/lib/libc-[0-9]*"
186
187        readelf_cmd = glob.glob("/usr/local/*/binutils-bin/*/readelf")[0]
188
189        # We do not test binaries if they are built with Address Sanitizer
190        # because it is a separate testing tool.
191        no_asan_used = utils.system_output("%s -s "
192                                           "/opt/google/chrome/chrome | "
193                                           "egrep -q \"__asan_init\" || "
194                                           "echo no ASAN" % readelf_cmd)
195        if not no_asan_used:
196            logging.debug("ASAN detected on /opt/google/chrome/chrome. "
197                          "Will skip all checks.")
198            return
199
200        # Check that gold was used to build binaries.
201        # TODO(jorgelo): re-enable this check once crbug.com/417912 is fixed.
202        # gold_cmd = ("%s -S {} 2>&1 | "
203        #             "egrep -q \".note.gnu.gold-ve\"" % readelf_cmd)
204        # gold_find_options = ""
205        # if utils.get_cpu_arch() == "arm":
206        #     # gold is only enabled for Chrome on ARM.
207        #     gold_find_options = "-path \"/opt/google/chrome/chrome\""
208        # gold_whitelist = os.path.join(self.bindir, "gold_whitelist")
209        # option_sets.append(self.create_and_filter("gold",
210        #                                           gold_cmd,
211        #                                           gold_whitelist,
212        #                                           gold_find_options))
213
214        # Verify non-static binaries have BIND_NOW in dynamic section.
215        now_cmd = ("(%s {} | grep -q statically) ||"
216                   "%s -d {} 2>&1 | "
217                   "egrep -q \"BIND_NOW\"" % (FILE_CMD, readelf_cmd))
218        if utils.is_freon():
219            now_whitelist = os.path.join(self.bindir, "now_whitelist")
220        else:
221            now_whitelist = os.path.join(self.bindir, "now_whitelist_x")
222        option_sets.append(self.create_and_filter("-Wl,-z,now",
223                                                  now_cmd,
224                                                  now_whitelist))
225
226        # Verify non-static binaries have RELRO program header.
227        relro_cmd = ("(%s {} | grep -q statically) ||"
228                     "%s -l {} 2>&1 | "
229                     "egrep -q \"GNU_RELRO\"" % (FILE_CMD, readelf_cmd))
230        relro_whitelist = os.path.join(self.bindir, "relro_whitelist")
231        option_sets.append(self.create_and_filter("-Wl,-z,relro",
232                                                  relro_cmd,
233                                                  relro_whitelist))
234
235        # Verify non-static binaries are dynamic (built PIE).
236        pie_cmd = ("(%s {} | grep -q statically) ||"
237                   "%s -l {} 2>&1 | "
238                   "egrep -q \"Elf file type is DYN\"" % (FILE_CMD,
239                                                          readelf_cmd))
240        pie_whitelist = os.path.join(self.bindir, "pie_whitelist")
241        option_sets.append(self.create_and_filter("-fPIE",
242                                                  pie_cmd,
243                                                  pie_whitelist))
244
245        # Verify all binaries have non-exec STACK program header.
246        stack_cmd = ("%s -lW {} 2>&1 | "
247                     "egrep -q \"GNU_STACK.*RW \"" % readelf_cmd)
248        stack_whitelist = os.path.join(self.bindir, "stack_whitelist")
249        option_sets.append(self.create_and_filter("Executable Stack",
250                                                  stack_cmd,
251                                                  stack_whitelist))
252
253        # Verify all binaries have W^X LOAD program headers.
254        loadwx_cmd = ("%s -lW {} 2>&1 | "
255                      "grep \"LOAD\" | egrep -v \"(RW |R E)\" | "
256                      "wc -l | grep -q \"^0$\"" % readelf_cmd)
257        if utils.is_freon():
258            loadwx_whitelist = os.path.join(self.bindir, "loadwx_whitelist")
259        else:
260            loadwx_whitelist = os.path.join(self.bindir, "loadwx_whitelist_x")
261        option_sets.append(self.create_and_filter("LOAD Writable and Exec",
262                                                  loadwx_cmd,
263                                                  loadwx_whitelist))
264
265        # Verify ARM binaries are all using VFP registers.
266        if (options.enable_hardfp and utils.get_cpu_arch() == 'arm'):
267            hardfp_cmd = ("%s -A {} 2>&1 | "
268                          "egrep -q \"Tag_ABI_VFP_args: VFP registers\"" %
269                          readelf_cmd)
270            hardfp_whitelist = os.path.join(self.bindir, "hardfp_whitelist")
271            option_sets.append(self.create_and_filter("hardfp", hardfp_cmd,
272                                                      hardfp_whitelist))
273
274        fail_msg = ""
275
276        # There is currently no way to clear binary prebuilts for all devs.
277        # Thus, when a new check is added to this test, the test might fail
278        # for users who have old prebuilts which have not been compiled
279        # in the correct manner.
280        fail_summaries = []
281        full_msg = "Test results:"
282        num_fails = 0
283        for cos in option_sets:
284            if len(cos.filtered_set):
285                num_fails += 1
286                fail_msg += cos.get_fail_message() + "\n"
287                fail_summaries.append(cos.get_fail_summary_message())
288            full_msg += str(cos) + "\n\n"
289        fail_summary_msg = ", ".join(fail_summaries)
290
291        logging.error(fail_msg)
292        logging.debug(full_msg)
293        if num_fails:
294            raise error.TestFail(fail_summary_msg)
295