• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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                                 allow_gbb_force=True)
87
88    def run_cmd(self, command):
89        """
90        Log and execute command and return the output.
91
92        @param command: Command to executeon device.
93        @returns the output of command.
94
95        """
96        logging.info('Execute %s', command)
97        output = self.faft_client.system.run_shell_command_get_output(command)
98        logging.info('Output %s', output)
99        return output
100
101    def _has_target(self, name):
102        """Return True if flashrom supports the programmer specified."""
103        return self.faft_client.system.run_shell_command_get_status(
104            'flashrom -p %s' % name) == 0
105
106    def get_areas(self):
107        """Get a list of dicts containing area names, offsets, and sizes
108        per device.
109
110        It fetches the FMap data from the active firmware via flashrom.
111        Stores the result in the appropriate _TARGET_AREA.
112        """
113        for target in self._TARGET_AREA:
114            if not self._has_target(target):
115                continue
116            tmpdir = self.faft_client.system.create_temp_dir('flashrom_')
117            fmap = os.path.join(tmpdir, 'fmap.bin')
118            self.run_cmd(
119                'flashrom -p %s -r -i FMAP:%s' % (target, fmap))
120            lines = self.run_cmd('dump_fmap -p %s' % fmap)
121            # Change the expected FMAP Tree if separate CBFS is used for CSE RW
122            command = "dump_fmap -F %s | grep ME_RW_A" % fmap
123            if (target in TARGET_BIOS) and  self.run_cmd(command):
124                self._EXPECTED_FMAP_TREE[target]['RW_SECTION_A'].update(
125                                                          INTEL_CSE_RW_A)
126                self._EXPECTED_FMAP_TREE[target]['RW_SECTION_B'].update(
127                                                          INTEL_CSE_RW_B)
128                logging.info("DUT uses INTEL CSE LITE FMAP Scheme")
129
130            self.faft_client.system.remove_dir(tmpdir)
131
132            # The above output is formatted as:
133            # name1 offset1 size1
134            # name2 offset2 size2
135            # ...
136            # Convert it to a list of dicts like:
137            # [{'name': name1, 'offset': offset1, 'size': size1},
138            #  {'name': name2, 'offset': offset2, 'size': size2}, ...]
139            for line in lines:
140                self._TARGET_AREA[target].append(
141                    dict(zip(FMAP_AREA_NAMES, line.split())))
142
143    def _is_bounded(self, region, bounds):
144        """Is the given region bounded by the given bounds?"""
145        return ((bounds[0] <= region[0] < bounds[1]) and
146                (bounds[0] < region[1] <= bounds[1]))
147
148
149    def _is_overlapping(self, region1, region2):
150        """Is the given region1 overlapping region2?"""
151        return (min(region1[1], region2[1]) > max(region1[0], region2[0]))
152
153
154    def check_section(self):
155        """Check RW_SECTION_[AB], RW_LEGACY and SMMSTORE.
156
157        1- check RW_SECTION_[AB] exist, non-zero, same size
158        2- RW_LEGACY exists and >= 1MB in size
159        3- optionally check SMMSTORE exists and >= 256KB in size
160        """
161        # Parse map into dictionary.
162        bios = {}
163        for e in self._TARGET_AREA[TARGET_BIOS]:
164            bios[e['name']] = {'offset': e['offset'], 'size': e['size']}
165        succeed = True
166        # Check RW_SECTION_[AB] sections.
167        if 'RW_SECTION_A' not in bios:
168            succeed = False
169            logging.error('Missing RW_SECTION_A section in FMAP')
170        elif 'RW_SECTION_B' not in bios:
171            succeed = False
172            logging.error('Missing RW_SECTION_B section in FMAP')
173        else:
174            if bios['RW_SECTION_A']['size'] != bios['RW_SECTION_B']['size']:
175                succeed = False
176                logging.error('RW_SECTION_A size != RW_SECTION_B size')
177            if (int(bios['RW_SECTION_A']['size']) == 0
178                    or int(bios['RW_SECTION_B']['size']) == 0):
179                succeed = False
180                logging.error('RW_SECTION_A size or RW_SECTION_B size == 0')
181        # Check RW_LEGACY section.
182        if 'RW_LEGACY' not in bios:
183            succeed = False
184            logging.error('Missing RW_LEGACY section in FMAP')
185        else:
186            if int(bios['RW_LEGACY']['size']) < 1024*1024:
187                succeed = False
188                logging.error('RW_LEGACY size is < 1M')
189        # Check SMMSTORE section.
190        if self.faft_config.smm_store and 'x86' in self.run_cmd('uname -m')[0]:
191            if 'SMMSTORE' not in bios:
192                succeed = False
193                logging.error('Missing SMMSTORE section in FMAP')
194            else:
195                if int(bios['SMMSTORE']['size']) < 256*1024:
196                    succeed = False
197                    logging.error('SMMSTORE size is < 256KB')
198
199        if not succeed:
200            raise error.TestFail('SECTION check failed.')
201
202
203    def check_areas(self, areas, expected_tree, bounds=None):
204        """Check the given area list met the hierarchy of the expected_tree.
205
206        It checks all areas in the expected tree are existed and non-zero sized.
207        It checks all areas in sub-trees are bounded by the region of the root
208        node. It also checks all areas in child nodes are mutually exclusive.
209
210        @param areas: A list of dicts containing area names, offsets, and sizes.
211        @param expected_tree: A hierarchy dict of the expected FMap tree.
212        @param bounds: The boards that all areas in the expect_tree are bounded.
213                       If None, ignore the bounds check.
214
215        >>> f = FMap()
216        >>> a = [{'name': 'FOO', 'offset': 100, 'size': '200'},
217        ...      {'name': 'BAR', 'offset': 100, 'size': '50'},
218        ...      {'name': 'ZEROSIZED', 'offset': 150, 'size': '0'},
219        ...      {'name': 'OUTSIDE', 'offset': 50, 'size': '50'}]
220        ...      {'name': 'OVERLAP', 'offset': 120, 'size': '50'},
221        >>> f.check_areas(a, {'FOO': {}})
222        True
223        >>> f.check_areas(a, {'NOTEXISTED': {}})
224        False
225        >>> f.check_areas(a, {'ZEROSIZED': {}})
226        False
227        >>> f.check_areas(a, {'BAR': {}, 'OVERLAP': {}})
228        False
229        >>> f.check_areas(a, {'FOO': {}, 'BAR': {}})
230        False
231        >>> f.check_areas(a, {'FOO': {}, 'OUTSIDE': {}})
232        True
233        >>> f.check_areas(a, {'FOO': {'BAR': {}}})
234        True
235        >>> f.check_areas(a, {'FOO': {'OUTSIDE': {}}})
236        False
237        >>> f.check_areas(a, {'FOO': {'NOTEXISTED': {}}})
238        False
239        >>> f.check_areas(a, {'FOO': {'ZEROSIZED': {}}})
240        False
241        """
242
243        succeed = True
244        checked_regions = []
245        for branch in expected_tree:
246            area = next((a for a in areas if a['name'] == branch), None)
247            if not area:
248                logging.error("The area %s is not existed.", branch)
249                succeed = False
250                continue
251            region = [int(area['offset']),
252                      int(area['offset']) + int(area['size'])]
253            if int(area['size']) == 0:
254                logging.error("The area %s is zero-sized.", branch)
255                succeed = False
256            elif bounds and not self._is_bounded(region, bounds):
257                logging.error("The region %s [%d, %d) is out of the bounds "
258                              "[%d, %d).", branch, region[0], region[1],
259                              bounds[0], bounds[1])
260                succeed = False
261            elif any(r for r in checked_regions if self._is_overlapping(
262                    region, r)):
263                logging.error("The area %s is overlapping others.", branch)
264                succeed = False
265            elif not self.check_areas(areas, expected_tree[branch], region):
266                succeed = False
267            checked_regions.append(region)
268        return succeed
269
270
271    def run_once(self):
272        """Runs a single iteration of the test."""
273        self.get_areas()
274
275        for key in self._TARGET_AREA.keys():
276            if (self._TARGET_AREA[key] and
277                    not self.check_areas(self._TARGET_AREA[key],
278                                         self._EXPECTED_FMAP_TREE[key])):
279                raise error.TestFail("%s FMap is not qualified.", key)
280        self.check_section()
281