1# Copyright (c) 2013 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 logging
6import os
7
8from autotest_lib.client.common_lib import error
9from autotest_lib.server.cros.faft.firmware_test import FirmwareTest
10
11TARGET_BIOS = 'host'
12TARGET_EC = 'ec'
13
14FMAP_AREA_NAMES = [
15    'name',
16    'offset',
17    'size'
18]
19
20EXPECTED_FMAP_TREE_BIOS = {
21  'WP_RO': {
22    'RO_SECTION': {
23      'FMAP': {},
24      'GBB': {},
25      'RO_FRID': {},
26    },
27    'RO_VPD': {},
28  },
29  'RW_SECTION_A': {
30    'VBLOCK_A': {},
31    'FW_MAIN_A': {},
32    'RW_FWID_A': {},
33  },
34  'RW_SECTION_B': {
35    'VBLOCK_B': {},
36    'FW_MAIN_B': {},
37    'RW_FWID_B': {},
38  },
39  'RW_VPD': {},
40}
41
42INTEL_CSE_RW_A = {
43   'ME_RW_A': {},
44}
45
46INTEL_CSE_RW_B = {
47   'ME_RW_B': {},
48}
49
50EXPECTED_FMAP_TREE_EC = {
51  'WP_RO': {
52    'EC_RO': {
53      'FMAP': {},
54      'RO_FRID': {},
55    },
56  },
57  'EC_RW': {
58    'RW_FWID': {},
59  },
60}
61
62class firmware_FMap(FirmwareTest):
63    """Provides access to firmware FMap"""
64
65    _TARGET_AREA = {
66        TARGET_BIOS: [],
67        TARGET_EC: [],
68    }
69
70    _EXPECTED_FMAP_TREE = {
71        TARGET_BIOS: EXPECTED_FMAP_TREE_BIOS,
72        TARGET_EC: EXPECTED_FMAP_TREE_EC,
73    }
74
75    """Client-side FMap test.
76
77    This test checks the active BIOS and EC firmware contains the required
78    FMap areas and verifies their hierarchies. It relies on flashrom to dump
79    the active BIOS and EC firmware and dump_fmap to decode them.
80    """
81    version = 1
82
83    def initialize(self, host, cmdline_args, dev_mode=False):
84        super(firmware_FMap, self).initialize(host, cmdline_args)
85        self.switcher.setup_mode('dev' if dev_mode else 'normal')
86
87    def run_cmd(self, command):
88        """
89        Log and execute command and return the output.
90
91        @param command: Command to executeon device.
92        @returns the output of command.
93
94        """
95        logging.info('Execute %s', command)
96        output = self.faft_client.system.run_shell_command_get_output(command)
97        logging.info('Output %s', output)
98        return output
99
100    def _has_target(self, name):
101        """Return True if flashrom supports the programmer specified."""
102        return self.faft_client.system.run_shell_command_get_status(
103            'flashrom -p %s' % name) == 0
104
105    def get_areas(self):
106        """Get a list of dicts containing area names, offsets, and sizes
107        per device.
108
109        It fetches the FMap data from the active firmware via flashrom.
110        Stores the result in the appropriate _TARGET_AREA.
111        """
112        for target in self._TARGET_AREA:
113            if not self._has_target(target):
114                continue
115            tmpdir = self.faft_client.system.create_temp_dir('flashrom_')
116            fmap = os.path.join(tmpdir, 'fmap.bin')
117            self.run_cmd(
118                'flashrom -p %s -r -i FMAP:%s' % (target, fmap))
119            lines = self.run_cmd('dump_fmap -p %s' % fmap)
120            # Change the expected FMAP Tree if separate CBFS is used for CSE RW
121            command = "dump_fmap -F %s | grep ME_RW_A" % fmap
122            if (target in TARGET_BIOS) and  self.run_cmd(command):
123                self._EXPECTED_FMAP_TREE[target]['RW_SECTION_A'].update(
124                                                          INTEL_CSE_RW_A)
125                self._EXPECTED_FMAP_TREE[target]['RW_SECTION_B'].update(
126                                                          INTEL_CSE_RW_B)
127                logging.info("DUT uses INTEL CSE LITE FMAP Scheme")
128
129            self.faft_client.system.remove_dir(tmpdir)
130
131            # The above output is formatted as:
132            # name1 offset1 size1
133            # name2 offset2 size2
134            # ...
135            # Convert it to a list of dicts like:
136            # [{'name': name1, 'offset': offset1, 'size': size1},
137            #  {'name': name2, 'offset': offset2, 'size': size2}, ...]
138            for line in lines:
139                self._TARGET_AREA[target].append(
140                    dict(zip(FMAP_AREA_NAMES, line.split())))
141
142    def _is_bounded(self, region, bounds):
143        """Is the given region bounded by the given bounds?"""
144        return ((bounds[0] <= region[0] < bounds[1]) and
145                (bounds[0] < region[1] <= bounds[1]))
146
147
148    def _is_overlapping(self, region1, region2):
149        """Is the given region1 overlapping region2?"""
150        return (min(region1[1], region2[1]) > max(region1[0], region2[0]))
151
152
153    def check_section(self):
154        """Check RW_SECTION_[AB], RW_LEGACY and SMMSTORE.
155
156        1- check RW_SECTION_[AB] exist, non-zero, same size
157        2- RW_LEGACY exists and >= 1MB in size
158        3- optionally check SMMSTORE exists and >= 256KB in size
159        """
160        # Parse map into dictionary.
161        bios = {}
162        for e in self._TARGET_AREA[TARGET_BIOS]:
163           bios[e['name']] = {'offset': e['offset'], 'size': e['size']}
164        succeed = True
165        # Check RW_SECTION_[AB] sections.
166        if 'RW_SECTION_A' not in bios:
167            succeed = False
168            logging.error('Missing RW_SECTION_A section in FMAP')
169        elif 'RW_SECTION_B' not in bios:
170            succeed = False
171            logging.error('Missing RW_SECTION_B section in FMAP')
172        else:
173            if bios['RW_SECTION_A']['size'] != bios['RW_SECTION_B']['size']:
174                succeed = False
175                logging.error('RW_SECTION_A size != RW_SECTION_B size')
176            if (int(bios['RW_SECTION_A']['size']) == 0
177                    or int(bios['RW_SECTION_B']['size']) == 0):
178                succeed = False
179                logging.error('RW_SECTION_A size or RW_SECTION_B size == 0')
180        # Check RW_LEGACY section.
181        if 'RW_LEGACY' not in bios:
182            succeed = False
183            logging.error('Missing RW_LEGACY section in FMAP')
184        else:
185            if int(bios['RW_LEGACY']['size']) < 1024*1024:
186                succeed = False
187                logging.error('RW_LEGACY size is < 1M')
188        # Check SMMSTORE section.
189        if self.faft_config.smm_store and 'x86' in self.run_cmd('uname -m')[0]:
190            if 'SMMSTORE' not in bios:
191                succeed = False
192                logging.error('Missing SMMSTORE section in FMAP')
193            else:
194                if int(bios['SMMSTORE']['size']) < 256*1024:
195                    succeed = False
196                    logging.error('SMMSTORE size is < 256KB')
197
198        if not succeed:
199            raise error.TestFail('SECTION check failed.')
200
201
202    def check_areas(self, areas, expected_tree, bounds=None):
203        """Check the given area list met the hierarchy of the expected_tree.
204
205        It checks all areas in the expected tree are existed and non-zero sized.
206        It checks all areas in sub-trees are bounded by the region of the root
207        node. It also checks all areas in child nodes are mutually exclusive.
208
209        @param areas: A list of dicts containing area names, offsets, and sizes.
210        @param expected_tree: A hierarchy dict of the expected FMap tree.
211        @param bounds: The boards that all areas in the expect_tree are bounded.
212                       If None, ignore the bounds check.
213
214        >>> f = FMap()
215        >>> a = [{'name': 'FOO', 'offset': 100, 'size': '200'},
216        ...      {'name': 'BAR', 'offset': 100, 'size': '50'},
217        ...      {'name': 'ZEROSIZED', 'offset': 150, 'size': '0'},
218        ...      {'name': 'OUTSIDE', 'offset': 50, 'size': '50'}]
219        ...      {'name': 'OVERLAP', 'offset': 120, 'size': '50'},
220        >>> f.check_areas(a, {'FOO': {}})
221        True
222        >>> f.check_areas(a, {'NOTEXISTED': {}})
223        False
224        >>> f.check_areas(a, {'ZEROSIZED': {}})
225        False
226        >>> f.check_areas(a, {'BAR': {}, 'OVERLAP': {}})
227        False
228        >>> f.check_areas(a, {'FOO': {}, 'BAR': {}})
229        False
230        >>> f.check_areas(a, {'FOO': {}, 'OUTSIDE': {}})
231        True
232        >>> f.check_areas(a, {'FOO': {'BAR': {}}})
233        True
234        >>> f.check_areas(a, {'FOO': {'OUTSIDE': {}}})
235        False
236        >>> f.check_areas(a, {'FOO': {'NOTEXISTED': {}}})
237        False
238        >>> f.check_areas(a, {'FOO': {'ZEROSIZED': {}}})
239        False
240        """
241
242        succeed = True
243        checked_regions = []
244        for branch in expected_tree:
245            area = next((a for a in areas if a['name'] == branch), None)
246            if not area:
247                logging.error("The area %s is not existed.", branch)
248                succeed = False
249                continue
250            region = [int(area['offset']),
251                      int(area['offset']) + int(area['size'])]
252            if int(area['size']) == 0:
253                logging.error("The area %s is zero-sized.", branch)
254                succeed = False
255            elif bounds and not self._is_bounded(region, bounds):
256                logging.error("The region %s [%d, %d) is out of the bounds "
257                              "[%d, %d).", branch, region[0], region[1],
258                              bounds[0], bounds[1])
259                succeed = False
260            elif any(r for r in checked_regions if self._is_overlapping(
261                    region, r)):
262                logging.error("The area %s is overlapping others.", branch)
263                succeed = False
264            elif not self.check_areas(areas, expected_tree[branch], region):
265                succeed = False
266            checked_regions.append(region)
267        return succeed
268
269
270    def run_once(self):
271        """Runs a single iteration of the test."""
272        self.get_areas()
273
274        for key in self._TARGET_AREA.keys():
275            if (self._TARGET_AREA[key] and
276                    not self.check_areas(self._TARGET_AREA[key],
277                                         self._EXPECTED_FMAP_TREE[key])):
278                raise error.TestFail("%s FMap is not qualified.", key)
279        self.check_section()
280