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